diff --git a/README.md b/README.md index 11bda1a..d4806ea 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Tu peux interrompre la génération à tout moment depuis le même panneau. - VS Code 1.85 ou plus récent - Un dépôt Git avec des changements stagés -- Une clé API pour le fournisseur choisi (non requise pour Ollama) +- Une clé API pour le fournisseur choisi (non requise pour Ollama et Claude Code) ## Installation @@ -39,8 +39,8 @@ Les paramètres se trouvent dans les préférences VS Code sous **Zemit**. | Paramètre | Description | Défaut | |---|---|---| -| `zemit.provider` | Fournisseur d'IA : `anthropic`, `openai` ou `ollama` | `anthropic` | -| `zemit.apiKey` | Clé API du fournisseur (inutile pour Ollama) | _(vide)_ | +| `zemit.provider` | Fournisseur d'IA : `anthropic`, `openai`, `ollama` ou `claudecode` | `anthropic` | +| `zemit.apiKey` | Clé API du fournisseur (inutile pour Ollama et Claude Code) | _(vide)_ | | `zemit.model` | Modèle à utiliser | `claude-sonnet-4-6` | | `zemit.baseUrl` | URL de base personnalisée (ex. Ollama local) | _(vide)_ | | `zemit.promptVersion` | Version du prompt | `zemit-v2` | @@ -61,6 +61,18 @@ Pour arrêter une génération en cours, clique sur l'icône d'arrêt au même e - **Anthropic** : modèles Claude (Haiku, Sonnet, Opus) - **OpenAI** : modèles GPT - **Ollama** : modèles locaux, aucune clé requise +- **Claude Code** : utilise le CLI `claude` installé localement, aucune clé API requise — l'authentification est gérée par Claude Code lui-même + +### Utiliser Claude Code + +Assure-toi que le CLI `claude` est installé et accessible (`~/.local/bin/claude` ou dans ton `PATH`), puis configure : + +```json +"zemit.provider": "claudecode", +"zemit.model": "claude-sonnet-4-6" +``` + +> Note : le provider Claude Code ne supporte pas le streaming — le message apparaît d'un coup à la fin de la génération, contrairement aux providers API. ## Développement diff --git a/package-lock.json b/package-lock.json index 0122ff4..e30b95e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "zemit", - "version": "1.1.4", + "version": "1.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "zemit", - "version": "1.1.4", + "version": "1.1.5", "license": "GPL-3.0-only", "devDependencies": { "@types/node": "^18.0.0", diff --git a/package.json b/package.json index fe6a6eb..f3a4ae2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zemit", - "version": "1.1.4", + "version": "1.1.5", "displayName": "Zemit", "description": "Génère des messages de commit, via un modèle d'IA, directement dans VSCode.", "repository": { @@ -36,7 +36,14 @@ "enum": [ "anthropic", "openai", - "ollama" + "ollama", + "claudecode" + ], + "enumDescriptions": [ + "Anthropic API (clé API requise)", + "OpenAI API (clé API requise)", + "Ollama – instance locale", + "Claude Code CLI – installation locale, sans clé API" ], "default": "anthropic", "description": "Fournisseur d'IA utilisé pour générer les messages de commit." @@ -44,7 +51,7 @@ "zemit.apiKey": { "type": "string", "default": "", - "markdownDescription": "Clé API du fournisseur sélectionné. Non requise pour Ollama." + "markdownDescription": "Clé API du fournisseur sélectionné. Non requise pour Ollama et Claude Code." }, "zemit.model": { "type": "string", diff --git a/src/providers.ts b/src/providers.ts index f21187b..db04fdc 100644 --- a/src/providers.ts +++ b/src/providers.ts @@ -1,3 +1,4 @@ +import { spawn } from "child_process" import * as vscode from "vscode" import { SYSTEM_PROMPT, getPrompt } from "./prompts" @@ -91,6 +92,69 @@ class AnthropicProvider implements AIProvider { } } +// ─── Claude Code CLI ───────────────────────────────────────────────────────── + +class ClaudeCodeProvider implements AIProvider { + constructor(private readonly model: string) {} + + async *generateCommitMessage(diff: string, style: string, signal: AbortSignal): AsyncIterable { + const instruction = getPrompt(style) + const input = `${instruction}\n\n${diff}` + + // ~/.local/bin may not be in PATH inside the VSCode extension host + const env = { ...process.env, PATH: `${process.env.HOME ?? ""}/.local/bin:${process.env.PATH ?? ""}` } + + const proc = spawn("claude", [ + "-p", "--output-format", "text", + "--model", this.model, + "--system-prompt", SYSTEM_PROMPT, + ], { env }) + + const onAbort = () => proc.kill() + signal.addEventListener("abort", onAbort, { once: true }) + + proc.stdin.end(input) + + let stderrData = "" + proc.stderr.on("data", (chunk: Buffer) => { stderrData += chunk.toString() }) + + // When spawn fails (e.g. ENOENT), 'close' never fires — resolve via 'error' too + let spawnError: Error | undefined + const closeOrError = new Promise((resolve) => { + proc.on("close", resolve) + proc.on("error", (err: Error) => { + spawnError = err + proc.stdout.destroy() // Unblock the for-await below + resolve(null) + }) + }) + + try { + for await (const chunk of proc.stdout) { + yield (chunk as Buffer).toString() + } + } catch { + // stdout may throw if destroyed due to a spawn error — handled below + } finally { + signal.removeEventListener("abort", onAbort) + } + + await closeOrError + + if (spawnError) { + const isNotFound = (spawnError as NodeJS.ErrnoException).code === "ENOENT" + throw new Error(isNotFound + ? "Claude Code CLI introuvable. Assurez-vous que 'claude' est installé et accessible (ex. ~/.local/bin)." + : `Claude Code CLI: ${spawnError.message}`) + } + + const exitCode = await closeOrError + if (exitCode !== 0 && !signal.aborted) { + throw new Error(`Claude Code CLI error: ${stderrData || `exit code ${exitCode}`}`) + } + } +} + // ─── SSE parsing helpers ───────────────────────────────────────────────────── async function* parseSSEStream( @@ -161,6 +225,9 @@ export function createProvider(config: vscode.WorkspaceConfiguration): AIProvide if (!apiKey) throw new Error("Clé API Anthropic requise. Configurez-la dans Paramètres → Zemit AI Commit.") return new AnthropicProvider(apiKey, model, baseUrl) } + case "claudecode": { + return new ClaudeCodeProvider(model) + } case "ollama": { const baseUrl = customBaseUrl || "http://localhost:11434/v1" return new OpenAICompatibleProvider("ollama", model, baseUrl) @@ -207,6 +274,8 @@ export async function fetchAvailableModels(config: vscode.WorkspaceConfiguration .filter((id) => /^(gpt-|o1|o3|chatgpt-)/.test(id)) .sort() } + case "claudecode": + return [] case "ollama": { const baseUrl = customBaseUrl || "http://localhost:11434/v1" const response = await fetch(`${baseUrl}/models`) diff --git a/tsconfig.json b/tsconfig.json index f63bd2d..bad8024 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "target": "ES2020", "module": "commonjs", "lib": ["ES2020"], + "types": ["node"], "outDir": "dist", "rootDir": "src", "strict": true,