diff --git a/package-lock.json b/package-lock.json index d171179..2d74273 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "ai-commit", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ai-commit", - "version": "0.1.0", + "version": "0.1.1", "license": "MIT", "dependencies": { + "@anthropic-ai/sdk": "^0.68.0", "@google/generative-ai": "^0.21.0", "fs-extra": "^11.0.4", "openai": "^4.14.2", @@ -36,6 +37,35 @@ "vscode": "^1.77.0" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.68.0.tgz", + "integrity": "sha512-SMYAmbbiprG8k1EjEPMTwaTqssDT7Ae+jxcR5kWXiqTlbwMR2AthXtscEVWOHkRfyAV5+y3PFYTJRNa3OJWIEw==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -401,6 +431,7 @@ "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -820,6 +851,7 @@ "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -878,6 +910,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1091,6 +1124,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001669", "electron-to-chromium": "^1.5.41", @@ -1565,6 +1599,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -2518,6 +2553,19 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -3879,6 +3927,12 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, "node_modules/ts-loader": { "version": "9.5.1", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", @@ -3955,6 +4009,7 @@ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4061,6 +4116,7 @@ "integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", @@ -4108,6 +4164,7 @@ "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", diff --git a/package.json b/package.json index 5a27c4a..3cc8bc6 100644 --- a/package.json +++ b/package.json @@ -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", @@ -14,6 +14,9 @@ "Azure", "OpenAI", "ChatGPT", + "Gemini", + "Claude", + "Anthropic", "GitEmoji", "Git Commit", "Conventional Commits", @@ -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": { @@ -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" @@ -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", diff --git a/src/claude-utils.ts b/src/claude-utils.ts new file mode 100644 index 0000000..486b2f2 --- /dev/null +++ b/src/claude-utils.ts @@ -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} messages - The messages to send to Claude. + * @param {string} apiKey - The Anthropic API key. + * @returns {Promise} - A promise that resolves to the API response. + */ +async function callClaudeAPI(messages: any[], apiKey: string): Promise { + const configManager = ConfigurationManager.getInstance(); + const model = configManager.getConfig(ConfigKeys.CLAUDE_MODEL, 'claude-sonnet-4-5'); + const temperature = configManager.getConfig(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} messages - The messages to send to Claude. + * @returns {Promise} - A promise that resolves to the API response. + */ +async function callClaudeCLI(messages: any[]): Promise { + // 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} messages - The messages to send to Claude. + * @returns {Promise} - A promise that resolves to the API response. + */ +export async function ClaudeAPI(messages: any[]): Promise { + try { + const configManager = ConfigurationManager.getInstance(); + const apiKey = configManager.getConfig(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; + } +} diff --git a/src/config.ts b/src/config.ts index df0194a..a7fba4c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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', } /** diff --git a/src/generate-commit-msg.ts b/src/generate-commit-msg.ts index 397ea36..f3d9517 100644 --- a/src/generate-commit-msg.ts +++ b/src/generate-commit-msg.ts @@ -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. @@ -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(ConfigKeys.OPENAI_API_KEY); if (!openaiApiKey) { @@ -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);