15 Commits

Author SHA1 Message Date
hykocx 954ca5bd47 docs(changelog): release version 1.1.5 2026-05-01 19:17:10 -04:00
hykocx 030a7b1472 docs: add changelog with project history 2026-05-01 19:16:10 -04:00
hykocx 05879f9171 fix(git): remove --diff-filter=d to include deleted files in diffs 2026-05-01 19:13:17 -04:00
hykocx ca73d9683f feat(providers): add Claude Code CLI provider
- implement `ClaudeCodeProvider` using `claude -p` with plain text output
- register `claudecode` as a new provider enum option in package.json
- add `enumDescriptions` for all provider choices
- update API key description to note it is not required for Claude Code
- bump version to 1.1.5
2026-05-01 14:59:26 -04:00
hykocx e309fa3acd chore: bump version to 1.1.4 2026-04-24 12:38:38 -04:00
hykocx c870350c11 chore(config): update vscodeignore to exclude additional build artifacts 2026-04-24 12:38:01 -04:00
hykocx f823dc4803 docs(readme): update banner image and replace asset file
- update README.md to reference new preview.jpg instead of git-banner.png
- add assets/preview.jpg as replacement banner image
- remove deprecated assets/git-banner.png
2026-04-24 12:32:06 -04:00
hykocx cbceea72fa docs(readme): update description and settings table to reflect new prompt version
- update diff description to mention file summary context
- replace `zemit.commitStyle` setting with `zemit.promptVersion` in settings table
2026-04-24 12:26:06 -04:00
hykocx 4471c2b2b2 refactor(git,commitGenerator): add git context with stat and improve prompt structure
- introduce `getGitContext` returning both diff and name-status stat output
- update `generateForRepository` and `performGeneration` to consume new context
- prefix prompt with changed files stat block when available
- replace `commitStyle` config key with `promptVersion` using `LATEST_PROMPT_VERSION`
- fix first-commit diff logic to use else-branch instead of fallthrough checks
- update extension icon
2026-04-24 12:22:01 -04:00
hykocx f5f6a2f703 chore: bump version to 1.1.4 and update dependencies
- upgrade @vscode/vsce from 3.8.1 to 3.9.1 (drops node >= 22 requirement to >= 20)
- rename config key `zemit.commitStyle` to `zemit.promptVersion` with new enum values `zemit-v1`/`zemit-v2`
- bump package version from 1.1.3 to 1.1.4
2026-04-24 12:21:53 -04:00
hykocx f63b6770d2 docs(assets): update git banner and app icon images 2026-04-15 18:12:09 -04:00
hykocx a90ab0eca3 docs(readme): add banner image and fix asset base urls for packaging 2026-04-15 18:04:57 -04:00
hykocx 1bdd31ae0c chore: bump version to 1.1.3 2026-04-15 17:58:59 -04:00
hykocx 0697e279ca refactor(prompts): extract prompts to dedicated module and enhance instructions 2026-04-15 17:58:45 -04:00
hykocx 718cdf87c9 chore: update icon image 2026-04-15 16:04:07 -04:00
12 changed files with 307 additions and 57 deletions
+7 -3
View File
@@ -1,6 +1,10 @@
src/** .vscode/**
node_modules/** .git/**
.gitignore .gitignore
node_modules/**
*.vsix
assets/**
src/**
tsconfig.json tsconfig.json
cline-main/**
**/*.map **/*.map
package-lock.json
+68
View File
@@ -0,0 +1,68 @@
# Changelog
All notable changes to Zemit are documented here.
## [1.1.5] - 2026-05-01
### Added
- Claude Code CLI provider (local, no API key required)
### Fixed
- File deletions were not detected when generating commit messages (`--diff-filter=d` excluded deleted files)
---
## [1.1.4] - 2026-04-24
### Changed
- Improved prompt structure: diff is now accompanied by a `--name-status` summary of changed files
- Updated README description and settings table to reflect new prompt version
- Updated extension icon and banner image
---
## [1.1.3] - 2026-04-15
### Changed
- Refactored prompt system into a dedicated `prompts` module with enhanced commit instructions
- Updated app icon and git banner images
---
## [1.1.2] - 2026-04-15
### Changed
- Updated extension icon
- Shortened marketplace description
---
## [1.1.1] - 2026-04-15
### Changed
- Translated all UI messages to French
- Updated default model to `claude-sonnet-4-6`
---
## [1.1.0] - 2026-04-15
### Added
- Dedicated `providers` module to support multiple AI backends
- Dedicated `git` module for staged/unstaged diff handling
- VS Code extension entry point with model selection
### Changed
- Refined conventional commit prompt to limit body usage
- Extracted commit generation logic into its own module
---
## [1.0.0] - 2026-04-15
### Added
- Initial release of Zemit
- AI-powered commit message generation using the Anthropic Claude API
- Support for staged and unstaged git diffs
- Conventional commit format by default
- GNU General Public License v3.0
+19 -5
View File
@@ -2,9 +2,11 @@
Génère des messages de commit, via un modèle d'IA, directement dans VSCode. Génère des messages de commit, via un modèle d'IA, directement dans VSCode.
![preview](/assets/preview.jpg)
## Ce que ça fait ## Ce que ça fait
Zemit lit le diff stagé dans ton dépôt Git et envoie le contenu à un modèle d'IA pour produire un message de commit. Le message apparaît directement dans le champ de saisie du panneau Source Control. Zemit lit le diff stagé et le résumé des fichiers modifiés dans ton dépôt Git, puis envoie ce contexte à un modèle d'IA pour produire un message de commit. Le message apparaît directement dans le champ de saisie du panneau Source Control.
Tu peux interrompre la génération à tout moment depuis le même panneau. Tu peux interrompre la génération à tout moment depuis le même panneau.
@@ -12,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 - VS Code 1.85 ou plus récent
- Un dépôt Git avec des changements stagés - 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 ## Installation
@@ -37,11 +39,11 @@ Les paramètres se trouvent dans les préférences VS Code sous **Zemit**.
| Paramètre | Description | Défaut | | Paramètre | Description | Défaut |
|---|---|---| |---|---|---|
| `zemit.provider` | Fournisseur d'IA : `anthropic`, `openai` ou `ollama` | `anthropic` | | `zemit.provider` | Fournisseur d'IA : `anthropic`, `openai`, `ollama` ou `claudecode` | `anthropic` |
| `zemit.apiKey` | Clé API du fournisseur (inutile pour Ollama) | _(vide)_ | | `zemit.apiKey` | Clé API du fournisseur (inutile pour Ollama et Claude Code) | _(vide)_ |
| `zemit.model` | Modèle à utiliser | `claude-sonnet-4-6` | | `zemit.model` | Modèle à utiliser | `claude-sonnet-4-6` |
| `zemit.baseUrl` | URL de base personnalisée (ex. Ollama local) | _(vide)_ | | `zemit.baseUrl` | URL de base personnalisée (ex. Ollama local) | _(vide)_ |
| `zemit.commitStyle` | Style du message : `conventional` ou `simple` | `conventional` | | `zemit.promptVersion` | Version du prompt | `zemit-v2` |
| `zemit.maxDiffSize` | Taille maximale du diff envoyé à l'IA (en caractères) | `5000` | | `zemit.maxDiffSize` | Taille maximale du diff envoyé à l'IA (en caractères) | `5000` |
Pour choisir un modèle parmi ceux disponibles chez ton fournisseur, lance la commande **Zemit: Select Model** depuis la palette de commandes. Pour choisir un modèle parmi ceux disponibles chez ton fournisseur, lance la commande **Zemit: Select Model** depuis la palette de commandes.
@@ -59,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) - **Anthropic** : modèles Claude (Haiku, Sonnet, Opus)
- **OpenAI** : modèles GPT - **OpenAI** : modèles GPT
- **Ollama** : modèles locaux, aucune clé requise - **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 ## Développement
Binary file not shown.

After

Width:  |  Height:  |  Size: 465 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

+6 -6
View File
@@ -1,12 +1,12 @@
{ {
"name": "zemit", "name": "zemit",
"version": "1.1.2", "version": "1.1.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "zemit", "name": "zemit",
"version": "1.1.2", "version": "1.1.5",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"devDependencies": { "devDependencies": {
"@types/node": "^18.0.0", "@types/node": "^18.0.0",
@@ -1038,9 +1038,9 @@
} }
}, },
"node_modules/@vscode/vsce": { "node_modules/@vscode/vsce": {
"version": "3.8.1", "version": "3.9.1",
"resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.8.1.tgz", "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.9.1.tgz",
"integrity": "sha512-Ij1i53rvR2Z/BR8tdESNqb5l5GNvOLQIWSbE1NnRnXQrvJu/xhK8nVfe6vXKdI6L7/fUwzlqqB1gOjt901mTmg==", "integrity": "sha512-MPn5p+DoudI+3GfJSpAZZraE1lgLv0LcwbH3+xy7RgEhty3UIkmUMUA+5jPTDaxXae00AnX5u77FxGM8FhfKKA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1078,7 +1078,7 @@
"vsce": "vsce" "vsce": "vsce"
}, },
"engines": { "engines": {
"node": ">= 22" "node": ">= 20"
}, },
"optionalDependencies": { "optionalDependencies": {
"keytar": "^7.7.0" "keytar": "^7.7.0"
+18 -11
View File
@@ -1,6 +1,6 @@
{ {
"name": "zemit", "name": "zemit",
"version": "1.1.2", "version": "1.1.5",
"displayName": "Zemit", "displayName": "Zemit",
"description": "Génère des messages de commit, via un modèle d'IA, directement dans VSCode.", "description": "Génère des messages de commit, via un modèle d'IA, directement dans VSCode.",
"repository": { "repository": {
@@ -36,7 +36,14 @@
"enum": [ "enum": [
"anthropic", "anthropic",
"openai", "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", "default": "anthropic",
"description": "Fournisseur d'IA utilisé pour générer les messages de commit." "description": "Fournisseur d'IA utilisé pour générer les messages de commit."
@@ -44,7 +51,7 @@
"zemit.apiKey": { "zemit.apiKey": {
"type": "string", "type": "string",
"default": "", "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": { "zemit.model": {
"type": "string", "type": "string",
@@ -56,18 +63,18 @@
"default": "", "default": "",
"markdownDescription": "URL de base personnalisée (ex. `http://localhost:11434/v1` pour Ollama). Laisser vide pour utiliser la valeur par défaut du fournisseur." "markdownDescription": "URL de base personnalisée (ex. `http://localhost:11434/v1` pour Ollama). Laisser vide pour utiliser la valeur par défaut du fournisseur."
}, },
"zemit.commitStyle": { "zemit.promptVersion": {
"type": "string", "type": "string",
"enum": [ "enum": [
"conventional", "zemit-v1",
"simple" "zemit-v2"
], ],
"enumDescriptions": [ "enumDescriptions": [
"Format Conventional Commits (feat:, fix:, chore:, etc.)", "Zemit V1 Format Conventional Commits classique",
"Description courte sur une ligne" "Zemit V2 Conventional Commits compact avec résumé de fichiers et corps structuré"
], ],
"default": "conventional", "default": "zemit-v2",
"description": "Format du message de commit généré." "description": "Version du prompt utilisé pour générer le message de commit."
}, },
"zemit.maxDiffSize": { "zemit.maxDiffSize": {
"type": "number", "type": "number",
@@ -116,7 +123,7 @@
"scripts": { "scripts": {
"build": "esbuild src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --platform=node --target=node18", "build": "esbuild src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --platform=node --target=node18",
"watch": "esbuild src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --platform=node --target=node18 --watch", "watch": "esbuild src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --platform=node --target=node18 --watch",
"package": "npm run build && vsce package" "package": "npm run build && vsce package --baseContentUrl https://git.hyko.cx/hykocx/zemit/raw/branch/main --baseImagesUrl https://git.hyko.cx/hykocx/zemit/raw/branch/main"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^18.0.0", "@types/node": "^18.0.0",
+7 -6
View File
@@ -1,6 +1,7 @@
import * as path from "path" import * as path from "path"
import * as vscode from "vscode" import * as vscode from "vscode"
import { getGitDiffStagedFirst } from "./git" import { getGitContext, getGitDiffStagedFirst } from "./git"
import { LATEST_PROMPT_VERSION } from "./prompts"
import { createProvider } from "./providers" import { createProvider } from "./providers"
let abortController: AbortController | undefined let abortController: AbortController | undefined
@@ -99,7 +100,7 @@ async function filterReposWithChanges(repos: unknown[]): Promise<unknown[]> {
async function generateForRepository(repository: any): Promise<void> { async function generateForRepository(repository: any): Promise<void> {
const repoPath = repository.rootUri.fsPath const repoPath = repository.rootUri.fsPath
const repoName = repoPath.split(path.sep).pop() || "repository" const repoName = repoPath.split(path.sep).pop() || "repository"
const diff = await getGitDiffStagedFirst(repoPath) const { diff, stat } = await getGitContext(repoPath)
await vscode.window.withProgress( await vscode.window.withProgress(
{ {
@@ -107,26 +108,26 @@ async function generateForRepository(repository: any): Promise<void> {
title: `Zemit : Génération du message de commit pour ${repoName}...`, title: `Zemit : Génération du message de commit pour ${repoName}...`,
cancellable: true, cancellable: true,
}, },
(_progress, token) => performGeneration(repository.inputBox, diff, token), (_progress, token) => performGeneration(repository.inputBox, diff, stat, token),
) )
} }
async function performGeneration( async function performGeneration(
inputBox: any, inputBox: any,
diff: string, diff: string,
stat: string,
token: vscode.CancellationToken, token: vscode.CancellationToken,
): Promise<void> { ): Promise<void> {
const config = vscode.workspace.getConfiguration("zemit") const config = vscode.workspace.getConfiguration("zemit")
const maxDiffSize = config.get<number>("maxDiffSize", 5000) const maxDiffSize = config.get<number>("maxDiffSize", 5000)
const style = config.get<string>("commitStyle", "conventional") const style = config.get<string>("promptVersion", LATEST_PROMPT_VERSION)
const truncatedDiff = const truncatedDiff =
diff.length > maxDiffSize ? diff.substring(0, maxDiffSize) + "\n\n[Diff truncated due to size]" : diff diff.length > maxDiffSize ? diff.substring(0, maxDiffSize) + "\n\n[Diff truncated due to size]" : diff
// Include any existing user note in the prompt
const existingNote = inputBox.value?.trim() || "" const existingNote = inputBox.value?.trim() || ""
let prompt = truncatedDiff let prompt = stat ? `## Changed files\n${stat}\n\n## Diff\n${truncatedDiff}` : truncatedDiff
if (existingNote) { if (existingNote) {
prompt = `Developer note (use if relevant): ${existingNote}\n\n${prompt}` prompt = `Developer note (use if relevant): ${existingNote}\n\n${prompt}`
} }
+58 -8
View File
@@ -41,19 +41,17 @@ export async function getGitDiff(cwd: string, stagedOnly = false): Promise<strin
let diff = "" let diff = ""
if (await hasCommits(cwd)) { if (await hasCommits(cwd)) {
const { stdout: staged } = await execAsync("git --no-pager diff --staged --diff-filter=d", { cwd }) const { stdout: staged } = await execAsync("git --no-pager diff --staged", { cwd })
diff = staged.trim() diff = staged.trim()
}
if (!stagedOnly && !diff) { if (!stagedOnly && !diff) {
const { stdout: unstaged } = await execAsync("git --no-pager diff HEAD --diff-filter=d", { cwd }) const { stdout: unstaged } = await execAsync("git --no-pager diff HEAD", { cwd })
diff = unstaged.trim() diff = unstaged.trim()
} }
} else {
// New repo with no commits yet — show everything that's staged/tracked // First commit — no HEAD exists yet
if (!diff) { const { stdout: cached } = await execAsync("git --no-pager diff --cached", { cwd })
const { stdout: newRepo } = await execAsync("git --no-pager diff --cached --diff-filter=d", { cwd }) diff = cached.trim()
diff = newRepo.trim()
} }
// Include untracked (new) files when not limiting to staged only // Include untracked (new) files when not limiting to staged only
@@ -85,3 +83,55 @@ export async function getGitDiffStagedFirst(cwd: string): Promise<string> {
return await getGitDiff(cwd, false) return await getGitDiff(cwd, false)
} }
} }
export async function getGitContext(cwd: string): Promise<{ diff: string; stat: string }> {
if (!(await isGitInstalled())) throw new Error("Git n'est pas installé")
if (!(await isGitRepo(cwd))) throw new Error("Pas un dépôt git")
let diff = ""
let stat = ""
let includeUntracked = false
if (await hasCommits(cwd)) {
const { stdout: stagedDiff } = await execAsync("git --no-pager diff --staged", { cwd })
diff = stagedDiff.trim()
if (diff) {
const { stdout: stagedStat } = await execAsync("git --no-pager diff --staged --name-status", { cwd })
stat = stagedStat.trim()
} else {
includeUntracked = true
const { stdout: allDiff } = await execAsync("git --no-pager diff HEAD", { cwd })
diff = allDiff.trim()
const { stdout: allStat } = await execAsync("git --no-pager diff HEAD --name-status", { cwd })
stat = allStat.trim()
}
} else {
// First commit — no HEAD exists yet
includeUntracked = true
const { stdout: cachedDiff } = await execAsync("git --no-pager diff --cached", { cwd })
diff = cachedDiff.trim()
const { stdout: cachedStat } = await execAsync("git --no-pager diff --cached --name-status", { cwd })
stat = cachedStat.trim()
}
if (includeUntracked) {
const { stdout: untrackedList } = await execAsync("git ls-files --others --exclude-standard", { cwd })
const untrackedFiles = untrackedList.trim().split("\n").filter(Boolean)
for (const file of untrackedFiles) {
const result = await execAsync(
`git --no-pager diff --no-index -- /dev/null ${JSON.stringify(file)}`,
{ cwd },
).catch((e: any) => ({ stdout: e.stdout as string | undefined }))
if (result.stdout) diff += (diff ? "\n" : "") + result.stdout
stat += (stat ? "\n" : "") + `A\t${file}`
}
}
diff = diff.trim()
stat = stat.trim()
if (!diff) throw new Error("Aucune modification trouvée pour générer un message de commit")
return { diff, stat }
}
+48
View File
@@ -0,0 +1,48 @@
export const LATEST_PROMPT_VERSION = "zemit-v2"
export const SYSTEM_PROMPT =
"You are a git commit message generator. Output only the commit message — no explanation, no preamble, no backticks, no quotes. All messages must be written in English."
export const ZEMIT_V1 = `Based on the provided git diff, generate a concise and descriptive commit message following the Conventional Commits format:
<type>(<scope>): <short description>
Types: feat, fix, refactor, style, docs, test, chore, perf, revert
- feat: new feature
- fix: bug fix
- refactor: code restructuring without behavior change
- style: formatting only (spaces, commas, no logic change)
- docs: documentation only
- test: add or update tests
- chore: maintenance, dependencies, build config
- perf: performance improvement
- revert: revert a previous commit (format: revert: revert "<original message>")
Rules:
- Scope is optional, specifies the affected area (e.g. auth, api, storage, ui, config)
- Description: lowercase, no trailing period, in English
- One commit = one intention, do not mix fix and refactor
- For breaking changes, add ! after the type and a BREAKING CHANGE: footer
- Only include a body if there are multiple distinct changes to explain; for a single focused change, output the title only`
export const ZEMIT_V2 = `Generate a git commit message following Conventional Commits format:
<type>(<scope>): <short description>
Types: feat | fix | refactor | style | docs | test | chore | perf
Scope: optional, affected module (e.g. auth, api, ui, config)
Description: lowercase, imperative mood, no trailing period
Body rules:
- Single focused change → title only
- Multiple files or distinct concerns → title + blank line + concise bullet body listing each change
- Breaking change: add ! after type + "BREAKING CHANGE: <detail>" footer`
const PROMPT_REGISTRY: Record<string, string> = {
"zemit-v1": ZEMIT_V1,
"zemit-v2": ZEMIT_V2,
}
export function getPrompt(version: string): string {
return PROMPT_REGISTRY[version] ?? PROMPT_REGISTRY[LATEST_PROMPT_VERSION]
}
+72 -15
View File
@@ -1,17 +1,6 @@
import { spawn } from "child_process"
import * as vscode from "vscode" import * as vscode from "vscode"
import { SYSTEM_PROMPT, getPrompt } from "./prompts"
const SYSTEM_PROMPT =
"You are a helpful assistant that generates informative git commit messages based on git diffs output. Skip preamble and remove all backticks surrounding the commit message."
const CONVENTIONAL_INSTRUCTION = `Based on the provided git diff, generate a concise and descriptive commit message.
The commit message should:
1. Have a short title (50-72 characters)
2. Follow the Conventional Commits format (feat:, fix:, chore:, docs:, refactor:, test:, style:, etc.)
3. Be clear and informative
4. Only include a body description if there are multiple distinct changes to explain, if the diff represents a single focused change, output the title only`
const SIMPLE_INSTRUCTION = `Based on the provided git diff, generate a short and clear one-line commit message (50-72 characters).`
export interface AIProvider { export interface AIProvider {
generateCommitMessage(diff: string, style: string, signal: AbortSignal): AsyncIterable<string> generateCommitMessage(diff: string, style: string, signal: AbortSignal): AsyncIterable<string>
@@ -27,7 +16,7 @@ class OpenAICompatibleProvider implements AIProvider {
) {} ) {}
async *generateCommitMessage(diff: string, style: string, signal: AbortSignal): AsyncIterable<string> { async *generateCommitMessage(diff: string, style: string, signal: AbortSignal): AsyncIterable<string> {
const instruction = style === "conventional" ? CONVENTIONAL_INSTRUCTION : SIMPLE_INSTRUCTION const instruction = getPrompt(style)
const url = `${this.baseUrl}/chat/completions` const url = `${this.baseUrl}/chat/completions`
const response = await fetch(url, { const response = await fetch(url, {
@@ -70,7 +59,7 @@ class AnthropicProvider implements AIProvider {
) {} ) {}
async *generateCommitMessage(diff: string, style: string, signal: AbortSignal): AsyncIterable<string> { async *generateCommitMessage(diff: string, style: string, signal: AbortSignal): AsyncIterable<string> {
const instruction = style === "conventional" ? CONVENTIONAL_INSTRUCTION : SIMPLE_INSTRUCTION const instruction = getPrompt(style)
const url = `${this.baseUrl}/messages` const url = `${this.baseUrl}/messages`
const response = await fetch(url, { const response = await fetch(url, {
@@ -103,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<string> {
const instruction = getPrompt(style)
const input = `<input>${instruction}\n\n${diff}</input>`
// ~/.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<number | null>((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 ───────────────────────────────────────────────────── // ─── SSE parsing helpers ─────────────────────────────────────────────────────
async function* parseSSEStream( async function* parseSSEStream(
@@ -173,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.") if (!apiKey) throw new Error("Clé API Anthropic requise. Configurez-la dans Paramètres → Zemit AI Commit.")
return new AnthropicProvider(apiKey, model, baseUrl) return new AnthropicProvider(apiKey, model, baseUrl)
} }
case "claudecode": {
return new ClaudeCodeProvider(model)
}
case "ollama": { case "ollama": {
const baseUrl = customBaseUrl || "http://localhost:11434/v1" const baseUrl = customBaseUrl || "http://localhost:11434/v1"
return new OpenAICompatibleProvider("ollama", model, baseUrl) return new OpenAICompatibleProvider("ollama", model, baseUrl)
@@ -219,6 +274,8 @@ export async function fetchAvailableModels(config: vscode.WorkspaceConfiguration
.filter((id) => /^(gpt-|o1|o3|chatgpt-)/.test(id)) .filter((id) => /^(gpt-|o1|o3|chatgpt-)/.test(id))
.sort() .sort()
} }
case "claudecode":
return []
case "ollama": { case "ollama": {
const baseUrl = customBaseUrl || "http://localhost:11434/v1" const baseUrl = customBaseUrl || "http://localhost:11434/v1"
const response = await fetch(`${baseUrl}/models`) const response = await fetch(`${baseUrl}/models`)
+1
View File
@@ -3,6 +3,7 @@
"target": "ES2020", "target": "ES2020",
"module": "commonjs", "module": "commonjs",
"lib": ["ES2020"], "lib": ["ES2020"],
"types": ["node"],
"outDir": "dist", "outDir": "dist",
"rootDir": "src", "rootDir": "src",
"strict": true, "strict": true,