Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 59 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 25 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "ai-commit",
"displayName": "AI Commit",
"description": "Use Azure/OpenAI API to review Git changes, generate conventional commit messages that meet the conventions, simplify the commit process, and keep the commit conventions consistent.",
"description": "Use Azure/OpenAI/Gemini/Claude API to review Git changes, generate conventional commit messages that meet the conventions, simplify the commit process, and keep the commit conventions consistent.",
"version": "0.1.1",
"engines": {
"node": ">=16",
Expand All @@ -14,6 +14,9 @@
"Azure",
"OpenAI",
"ChatGPT",
"Gemini",
"Claude",
"Anthropic",
"GitEmoji",
"Git Commit",
"Conventional Commits",
Expand Down Expand Up @@ -128,10 +131,11 @@
"ai-commit.AI_PROVIDER": {
"type": "string",
"default": "openai",
"description": "AI Provider to use (OpenAI or Gemini)",
"description": "AI Provider to use (OpenAI, Gemini, or Claude)",
"enum": [
"openai",
"gemini"
"gemini",
"claude"
]
},
"ai-commit.GEMINI_API_KEY": {
Expand All @@ -150,6 +154,23 @@
"minimum": 0,
"maximum": 2,
"description": "Gemini temperature setting (0-2). Controls randomness."
},
"ai-commit.CLAUDE_API_KEY": {
"type": "string",
"default": "",
"description": "Claude API Key (optional). Leave empty to use Claude CLI (authenticated via 'claude setup-token'), or provide an Anthropic API key for direct API access."
},
"ai-commit.CLAUDE_MODEL": {
"type": "string",
"default": "claude-sonnet-4-5-20250929",
"description": "Claude Model to use"
},
"ai-commit.CLAUDE_TEMPERATURE": {
"type": "number",
"default": 0.7,
"minimum": 0,
"maximum": 1,
"description": "Claude temperature setting (0-1). Controls randomness."
}
},
"title": "AI Commit"
Expand Down Expand Up @@ -195,6 +216,7 @@
"webpack-cli": "^5.0.1"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.68.0",
"@google/generative-ai": "^0.21.0",
"fs-extra": "^11.0.4",
"openai": "^4.14.2",
Expand Down
113 changes: 113 additions & 0 deletions src/claude-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import Anthropic from '@anthropic-ai/sdk';
import { ConfigKeys, ConfigurationManager } from './config';

const execAsync = promisify(exec);

/**
* Sends a chat completion request to Claude using the Anthropic API.
* @param {Array<Object>} messages - The messages to send to Claude.
* @param {string} apiKey - The Anthropic API key.
* @returns {Promise<string>} - A promise that resolves to the API response.
*/
async function callClaudeAPI(messages: any[], apiKey: string): Promise<string> {
const configManager = ConfigurationManager.getInstance();
const model = configManager.getConfig<string>(ConfigKeys.CLAUDE_MODEL, 'claude-sonnet-4-5');
const temperature = configManager.getConfig<number>(ConfigKeys.CLAUDE_TEMPERATURE, 0.7);

const anthropic = new Anthropic({ apiKey });

// Convert messages to Claude format
const systemMessage = messages.find(msg => msg.role === 'system');
const conversationMessages = messages
.filter(msg => msg.role !== 'system')
.map(msg => ({
role: msg.role as 'user' | 'assistant',
content: msg.content
}));

const response = await anthropic.messages.create({
model,
max_tokens: 1024,
temperature,
system: systemMessage?.content,
messages: conversationMessages
});

// Extract text from the response
const textContent = response.content.find(block => block.type === 'text');
if (textContent && textContent.type === 'text') {
return textContent.text;
}
return '';
}

/**
* Sends a chat completion request to Claude using the installed CLI command.
* @param {Array<Object>} messages - The messages to send to Claude.
* @returns {Promise<string>} - A promise that resolves to the API response.
*/
async function callClaudeCLI(messages: any[]): Promise<string> {
// Combine all messages into a single prompt
const systemMessage = messages.find(msg => msg.role === 'system');
const userMessages = messages.filter(msg => msg.role !== 'system');

let fullPrompt = '';

if (systemMessage) {
fullPrompt += systemMessage.content + '\n\n';
}

fullPrompt += userMessages.map(msg => msg.content).join('\n\n');

// Escape the prompt for shell
const escapedPrompt = fullPrompt.replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/`/g, '\\`');

// Call the claude CLI command
const { stdout, stderr } = await execAsync(`echo "${escapedPrompt}" | claude`, {
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
timeout: 60000 // 60 second timeout
});

if (stderr && !stdout) {
throw new Error(`Claude CLI error: ${stderr}`);
}

return stdout.trim();
}

/**
* Sends a chat completion request to Claude.
* Automatically chooses between API and CLI based on whether API key is configured.
* @param {Array<Object>} messages - The messages to send to Claude.
* @returns {Promise<string>} - A promise that resolves to the API response.
*/
export async function ClaudeAPI(messages: any[]): Promise<string> {
try {
const configManager = ConfigurationManager.getInstance();
const apiKey = configManager.getConfig<string>(ConfigKeys.CLAUDE_API_KEY, '');

// If API key is configured, use the Anthropic API
if (apiKey && apiKey.trim() !== '') {
return await callClaudeAPI(messages, apiKey);
}

// Otherwise, use the Claude CLI (requires 'claude setup-token')
return await callClaudeCLI(messages);

} catch (error: any) {
console.error('Claude API call failed:', error);

// Provide helpful error messages
if (error.code === 'ENOENT') {
throw new Error('Claude CLI not found. Please install Claude Code CLI or configure an API key: https://claude.com/claude-code');
}

if (error.message && error.message.includes('not authenticated')) {
throw new Error('Claude CLI not authenticated. Run: claude setup-token (or configure an API key in settings)');
}

throw error;
}
}
4 changes: 4 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export enum ConfigKeys {
GEMINI_MODEL = 'GEMINI_MODEL',
GEMINI_TEMPERATURE = 'GEMINI_TEMPERATURE',
AI_PROVIDER = 'AI_PROVIDER',

CLAUDE_API_KEY = 'CLAUDE_API_KEY',
CLAUDE_MODEL = 'CLAUDE_MODEL',
CLAUDE_TEMPERATURE = 'CLAUDE_TEMPERATURE',
}

/**
Expand Down
6 changes: 6 additions & 0 deletions src/generate-commit-msg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ChatGPTAPI } from './openai-utils';
import { getMainCommitPrompt } from './prompts';
import { ProgressHandler } from './utils';
import { GeminiAPI } from './gemini-utils';
import { ClaudeAPI } from './claude-utils';

/**
* Generates a chat completion prompt for the commit message based on the provided diff.
Expand Down Expand Up @@ -117,6 +118,9 @@ export async function generateCommitMsg(arg) {
throw new Error('Gemini API Key not configured');
}
commitMessage = await GeminiAPI(messages);
} else if (aiProvider === 'claude') {
// Claude uses CLI, no API key needed (already authenticated via 'claude setup-token')
commitMessage = await ClaudeAPI(messages);
} else {
const openaiApiKey = configManager.getConfig<string>(ConfigKeys.OPENAI_API_KEY);
if (!openaiApiKey) {
Expand Down Expand Up @@ -151,6 +155,8 @@ export async function generateCommitMsg(arg) {
}
} else if (aiProvider === 'gemini') {
errorMessage = `Gemini API error: ${err.message}`;
} else if (aiProvider === 'claude') {
errorMessage = `Claude API error: ${err.message}`;
}

throw new Error(errorMessage);
Expand Down