diff --git a/.env.example b/.env.example index e6bd98b..be96c1a 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,7 @@ # OpenAI API Key # Get yours at: https://platform.openai.com/api-keys OPENAI_API_KEY=sk-your-api-key-here + +# Gemini API Key +# Get yours at: https://aistudio.google.com/u/1/api-keys +GEMINI_API_KEY=gemini-api-key diff --git a/package.json b/package.json index 0d69dac..3cea0d9 100644 --- a/package.json +++ b/package.json @@ -47,16 +47,17 @@ "LICENSE" ], "dependencies": { - "openai": "^4.20.0", - "commander": "^11.1.0", + "@google/generative-ai": "^0.24.1", "chalk": "^5.3.0", - "ora": "^7.0.1", - "node-fetch": "^3.3.2" + "commander": "^11.1.0", + "node-fetch": "^3.3.2", + "openai": "^4.20.0", + "ora": "^7.0.1" }, "devDependencies": { "@types/node": "^20.10.0", - "typescript": "^5.3.2", - "tsx": "^4.6.2" + "tsx": "^4.6.2", + "typescript": "^5.3.2" }, "publishConfig": { "access": "public" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 969095c..3ab5490 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@google/generative-ai': + specifier: ^0.24.1 + version: 0.24.1 chalk: specifier: ^5.3.0 version: 5.6.2 @@ -192,6 +195,10 @@ packages: cpu: [x64] os: [win32] + '@google/generative-ai@0.24.1': + resolution: {integrity: sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==} + engines: {node: '>=18.0.0'} + '@types/node-fetch@2.6.13': resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} @@ -572,6 +579,8 @@ snapshots: '@esbuild/win32-x64@0.25.12': optional: true + '@google/generative-ai@0.24.1': {} + '@types/node-fetch@2.6.13': dependencies: '@types/node': 20.19.25 diff --git a/src/cli.ts b/src/cli.ts index 2d2481e..3de32c8 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -133,6 +133,13 @@ async function installCommitHooks(): Promise { console.log(chalk.gray(' npm install -g @anthropic-ai/claude-code')); console.log(chalk.gray(' claude login')); console.log(chalk.gray(' git config hooks.commitProvider claude-code')); + console.log(chalk.gray('\n # Option D: Gemini (sends data to Google API)')); + if (isWindows) { + console.log(chalk.gray(' set GEMINI_API_KEY="your-api-key"')); + } else { + console.log(chalk.gray(' export GEMINI_API_KEY="your-api-key"')); + } + console.log(chalk.gray(' git config hooks.commitProvider gemini')); console.log('\n3. Optional customizations:'); console.log(chalk.gray(' git config hooks.commitTemplate "type(scope): message"')); @@ -148,9 +155,9 @@ program .name('git-rewrite-commits') .description('AI-powered git commit message rewriter using OpenAI or Ollama') .version(packageJson.version) - .option('--provider ', 'AI provider to use: "openai", "ollama", or "claude-code"', 'openai') - .option('-k, --api-key ', 'OpenAI API key (defaults to OPENAI_API_KEY env var)') - .option('-m, --model ', 'AI model to use (default: gpt-3.5-turbo for OpenAI, llama3.2 for Ollama, haiku for Claude Code)') + .option('--provider ', 'AI provider to use: "openai", "ollama", "claude-code", or "gemini"', 'openai') + .option('-k, --api-key ', 'API key for the selected provider (defaults to OPENAI_API_KEY or GEMINI_API_KEY env var)') + .option('-m, --model ', 'AI model to use (default: gpt-3.5-turbo for OpenAI, llama3.2 for Ollama, haiku for Claude Code, gemini-2.5-flash-lite for Gemini)') .option('--ollama-url ', 'Ollama server URL', 'http://localhost:11434') .option('-b, --branch ', 'Branch to rewrite (defaults to current branch)') .option('-d, --dry-run', 'Show what would be changed without modifying repository') @@ -176,9 +183,8 @@ program // Check for API key if using OpenAI const provider = options.provider || 'openai'; - const apiKey = options.apiKey || process.env.OPENAI_API_KEY; - - if (provider === 'openai' && !apiKey) { + + if (provider === 'openai' && !options.apiKey && !process.env.OPENAI_API_KEY) { console.error(chalk.red('\n❌ Error: OpenAI API key is required!')); console.error(chalk.yellow('\nPlease provide it using one of these methods:')); console.error(chalk.cyan(' 1. Set environment variable: export OPENAI_API_KEY="your-api-key"')); @@ -187,6 +193,15 @@ program console.error(chalk.blue('\n💡 Tip: Use --provider ollama to use local models with Ollama instead')); process.exit(1); } + + if (provider === 'gemini' && !options.apiKey && !process.env.GEMINI_API_KEY) { + console.error(chalk.red('\n❌ Error: Gemini API key is required!')); + console.error(chalk.yellow('\nPlease provide it using one of these methods:')); + console.error(chalk.cyan(' 1. Set environment variable: export GEMINI_API_KEY="your-api-key"')); + console.error(chalk.cyan(' 2. Pass as argument: git-rewrite-commits --provider gemini --api-key "your-api-key"')); + console.error(chalk.dim('\nGet your API key at: https://aistudio.google.com/app/apikey')); + process.exit(1); + } // Only show informational messages when NOT in quiet mode if (provider === 'ollama' && !options.quiet) { @@ -199,9 +214,14 @@ program console.log(chalk.gray(' Make sure Claude Code is installed and authenticated: claude login')); } + if (provider === 'gemini' && !options.quiet) { + console.log(chalk.blue('ℹ️ Using Gemini provider')); + console.log(chalk.gray(' Make sure GEMINI_API_KEY is set or pass --api-key')); + } + const rewriter = new GitCommitRewriter({ provider: provider, - apiKey, + apiKey: options.apiKey || (provider === 'gemini' ? process.env.GEMINI_API_KEY : process.env.OPENAI_API_KEY), model: options.model, ollamaUrl: options.ollamaUrl, branch: options.branch, @@ -280,10 +300,14 @@ ${chalk.bold('Examples:')} ${chalk.gray('# Use Ollama with local models')} $ git-rewrite-commits --provider ollama --model llama3.2 - ${chalk.gray('# Use Claude Code CLI (no API key needed, uses Claude subscription)')} + ${chalk.gray('# Use Claude Code CLI (no API key needed, uses Claude subscription)')} $ git-rewrite-commits --provider claude-code $ git-rewrite-commits --provider claude-code --model opus + ${chalk.gray('# Use Gemini (gemini-2.5-flash-lite by default)')} + $ git-rewrite-commits --provider gemini + $ git-rewrite-commits --provider gemini --model gemini-2.5-flash-lite --api-key "your-key" + ${chalk.gray('# Custom prompt for specific requirements')} $ git-rewrite-commits --prompt "generate humorous but professional messages" @@ -292,6 +316,7 @@ ${chalk.bold('Examples:')} ${chalk.bold('Environment Variables:')} OPENAI_API_KEY Your OpenAI API key (required when using OpenAI provider) + GEMINI_API_KEY Your Gemini API key (required when using Gemini provider) ${chalk.bold('Important Notes:')} ${chalk.yellow('⚠️ This tool rewrites git history!')} diff --git a/src/index.ts b/src/index.ts index 99b4480..d57befd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ import ora from 'ora'; import { AIProvider, createProvider } from './providers/index.js'; export interface RewriteOptions { - provider?: 'openai' | 'ollama' | 'claude-code'; + provider?: 'openai' | 'ollama' | 'claude-code' | 'gemini'; apiKey?: string; model?: string; ollamaUrl?: string; @@ -41,6 +41,7 @@ export class GitCommitRewriter { const model = options.model || ( provider === 'ollama' ? 'llama3.2' : provider === 'claude-code' ? 'haiku' : + provider === 'gemini' ? 'gemini-2.5-flash-lite' : 'gpt-3.5-turbo' ); @@ -74,7 +75,7 @@ export class GitCommitRewriter { private execCommand(command: string): string { try { - return execSync(command, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 }); + return execSync(command, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] }); } catch (error: any) { throw new Error(`Command failed: ${command}\n${error.message}`); } diff --git a/src/providers/gemini.ts b/src/providers/gemini.ts new file mode 100644 index 0000000..6d76933 --- /dev/null +++ b/src/providers/gemini.ts @@ -0,0 +1,39 @@ +import { GoogleGenerativeAI } from '@google/generative-ai'; +import { AIProvider } from './types.js'; + +export class GeminiProvider implements AIProvider { + private genAI: GoogleGenerativeAI; + private model: string; + + constructor(apiKey: string, model: string = 'gemini-2.5-flash-lite') { + if (!apiKey) { + throw new Error('Gemini API key is required'); + } + this.genAI = new GoogleGenerativeAI(apiKey); + this.model = model; + } + + async generateCommitMessage(prompt: string, systemPrompt: string): Promise { + const generativeModel = this.genAI.getGenerativeModel({ + model: this.model, + systemInstruction: systemPrompt, + generationConfig: { + temperature: 0.3, + maxOutputTokens: 200, + }, + }); + + const result = await generativeModel.generateContent(prompt); + const message = result.response.text().trim(); + + if (!message) { + throw new Error('No commit message generated'); + } + + return message; + } + + getName(): string { + return `Gemini (${this.model})`; + } +} diff --git a/src/providers/index.ts b/src/providers/index.ts index 218f9ff..78c9aba 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -2,11 +2,13 @@ import { AIProvider, ProviderOptions } from './types.js'; import { OpenAIProvider } from './openai.js'; import { OllamaProvider } from './ollama.js'; import { ClaudeCodeProvider } from './claude-code.js'; +import { GeminiProvider } from './gemini.js'; export { AIProvider } from './types.js'; export { OpenAIProvider } from './openai.js'; export { OllamaProvider } from './ollama.js'; export { ClaudeCodeProvider } from './claude-code.js'; +export { GeminiProvider } from './gemini.js'; export function createProvider(options: ProviderOptions): AIProvider { const providerType = options.provider || 'openai'; @@ -15,6 +17,12 @@ export function createProvider(options: ProviderOptions): AIProvider { return new OllamaProvider(options.model || 'llama3.2', options.ollamaUrl); } else if (providerType === 'claude-code') { return new ClaudeCodeProvider(options.model || 'haiku'); + } else if (providerType === 'gemini') { + const apiKey = options.apiKey || process.env.GEMINI_API_KEY; + if (!apiKey) { + throw new Error('Gemini API key is required. Set GEMINI_API_KEY environment variable or pass it as an option.'); + } + return new GeminiProvider(apiKey, options.model || 'gemini-2.5-flash-lite'); } else { const apiKey = options.apiKey || process.env.OPENAI_API_KEY; if (!apiKey) { diff --git a/src/providers/types.ts b/src/providers/types.ts index 8823713..94a0797 100644 --- a/src/providers/types.ts +++ b/src/providers/types.ts @@ -4,7 +4,7 @@ export interface AIProvider { } export interface ProviderOptions { - provider?: 'openai' | 'ollama' | 'claude-code'; + provider?: 'openai' | 'ollama' | 'claude-code' | 'gemini'; apiKey?: string; model?: string; ollamaUrl?: string;