diff --git a/README.md b/README.md index 2b18797..4ded124 100644 --- a/README.md +++ b/README.md @@ -8,17 +8,23 @@ This package uses the power of OpenAI's GPT-4o-mini model to understand your code changes and generate meaningful commit messages for you. Whether you're working on a solo project or collaborating with a team, AI-Commit makes it easy to keep your commit history organized and informative. ## Demo -![ai_commit_demo(1)(2)](https://github.com/JinoArch/ai-commit/assets/39610834/3002dfa2-737a-44b9-91c9-b43907f11144) +![ai_commit_demo(1)(2)](https://github.com/JinoArch/ai-commit/assets/39610834/3002dfa2-737a-44b9-91c9-b43907f11144) ## How it Works + 1. Install AI-Commit using `npm install -g ai-commit` -2. Generate an OpenAI API key [here](https://platform.openai.com/account/api-keys ) -3. Set your `OPENAI_API_KEY` environment variable to your API key -1. Make your code changes and stage them with `git add .` -2. Type `ai-commit` in your terminal -3. AI-Commit will analyze your changes and generate a commit message -4. Approve the commit message and AI-Commit will create the commit for you ✅ +2. Generate an OpenAI API key [here](https://platform.openai.com/account/api-keys) +3. Set your `AI_COMMIT_API_KEY` environment variable to your API key +4. Set `PROVIDER` in your environment to `openai` or `gemini`. Default is `openai` +5. Make your code changes and stage them with `git add .` +6. Type `ai-commit` in your terminal +7. AI-Commit will analyze your changes and generate a commit message +8. Approve the commit message and AI-Commit will create the commit for you ✅ + +## Gemini Note + +We're using https://openrouter.ai/ and the model `google/gemini-2.0-flash-lite-preview-02-05:free` for Gemini, it support many models also free model, you can create account and try your own key without paying anything. ## Using local model (ollama) @@ -28,12 +34,13 @@ You can also use the local model for free with Ollama. 2. Install Ollama from https://ollama.ai/ 3. Run `ollama run mistral` to fetch model for the first time 4. Set `PROVIDER` in your environment to `ollama` -1. Make your code changes and stage them with `git add .` -2. Type `ai-commit` in your terminal -3. AI-Commit will analyze your changes and generate a commit message -4. Approve the commit message and AI-Commit will create the commit for you ✅ +5. Make your code changes and stage them with `git add .` +6. Type `ai-commit` in your terminal +7. AI-Commit will analyze your changes and generate a commit message +8. Approve the commit message and AI-Commit will create the commit for you ✅ ## Options + `--list`: Select from a list of 5 generated messages (or regenerate the list) `--force`: Automatically create a commit without being prompted to select a message (can't be used with `--list`) @@ -51,6 +58,7 @@ You can also use the local model for free with Ollama. `--commit-type`: Specify the type of commit to generate. This will be used as the type in the commit message e.g. `--commit-type feat` ## Contributing + We'd love for you to contribute to AI-Commit! Here's how: 1. Fork the repository @@ -73,6 +81,7 @@ We'd love for you to contribute to AI-Commit! Here's how: - [ ] Reverse commit message generation: Allow users to generate code changes from a commit message. ## License + AI-Commit is licensed under the MIT License. ## Happy coding 🚀 diff --git a/gemini.js b/gemini.js new file mode 100644 index 0000000..b5cd5a2 --- /dev/null +++ b/gemini.js @@ -0,0 +1,104 @@ +import inquirer from "inquirer"; +import { AI_PROVIDER } from "./config.js"; + +const FEE_PER_1K_TOKENS = 0.0; +const MAX_TOKENS = 1_000_000; +const FEE_COMPLETION = 0.001; + +const gemini = { + sendMessage: async ( + input, + { apiKey, model = "google/gemini-2.0-flash-lite-preview-02-05:free" } + ) => { + console.log("prompting Gemini API..."); + console.log("prompt: ", input); + + const response = await fetch( + "https://openrouter.ai/api/v1/chat/completions", + { + method: "POST", + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model, + messages: [ + { + role: "user", + content: [{ type: "text", text: input }], + }, + ], + }), + } + ); + + const data = await response.json(); + return data.choices?.[0]?.message?.content || ""; + }, + + getPromptForSingleCommit: ( + diff, + { commitType, customMessageConvention, language } + ) => { + return ( + `Write a professional git commit message based on the diff below in ${language} language` + + (commitType ? ` with commit type '${commitType}'. ` : ". ") + + `${ + customMessageConvention + ? `Apply these JSON formatted rules: ${customMessageConvention}.` + : "" + }` + + "Do not preface the commit with anything, use the present tense, return the full sentence and also commit type." + + `\n\n${diff}` + ); + }, + + getPromptForMultipleCommits: ( + diff, + { commitType, customMessageConvention, numOptions, language } + ) => { + return ( + `Write a professional git commit message based on the diff below in ${language} language` + + (commitType ? ` with commit type '${commitType}'. ` : ". ") + + `Generate ${numOptions} options separated by ";".` + + "For each option, use the present tense, return the full sentence and also commit type." + + `${ + customMessageConvention + ? ` Apply these JSON formatted rules: ${customMessageConvention}.` + : "" + }` + + `\n\n${diff}` + ); + }, + + filterApi: async ({ prompt, numCompletion = 1, filterFee }) => { + const numTokens = prompt.split(" ").length; // Approximate token count + const fee = + (numTokens / 1000) * FEE_PER_1K_TOKENS + FEE_COMPLETION * numCompletion; + + if (numTokens > MAX_TOKENS) { + console.log( + "The commit diff is too large for the Gemini API. Max 128k tokens." + ); + return false; + } + + // if (filterFee) { + // console.log(`This will cost you ~$${fee.toFixed(3)} for using the API.`); + // const answer = await inquirer.prompt([ + // { + // type: "confirm", + // name: "continue", + // message: "Do you want to continue 💸?", + // default: true, + // }, + // ]); + // if (!answer.continue) return false; + // } + + return true; + }, +}; + +export default gemini; diff --git a/index.js b/index.js index 4206168..35b1eb7 100755 --- a/index.js +++ b/index.js @@ -1,57 +1,72 @@ #!/usr/bin/env node -'use strict' +"use strict"; import { execSync } from "child_process"; import inquirer from "inquirer"; import { getArgs, checkGitRepository } from "./helpers.js"; -import { addGitmojiToCommitMessage } from './gitmoji.js'; -import { AI_PROVIDER, MODEL, args } from "./config.js" -import openai from "./openai.js" -import ollama from "./ollama.js" +import { addGitmojiToCommitMessage } from "./gitmoji.js"; +import { AI_PROVIDER, MODEL, args } from "./config.js"; +import openai from "./openai.js"; +import ollama from "./ollama.js"; +import gemini from "./gemini.js"; + +const PROVIDER_SUPPORT = { + openai, + ollama, + gemini, +}; const REGENERATE_MSG = "♻️ Regenerate Commit Messages"; -console.log('Ai provider: ', AI_PROVIDER); +console.log("Ai provider: ", AI_PROVIDER); -const ENDPOINT = args.ENDPOINT || process.env.ENDPOINT +const ENDPOINT = args.ENDPOINT || process.env.ENDPOINT; -const apiKey = args.apiKey || process.env.OPENAI_API_KEY; +const apiKey = args.apiKey || process.env.AI_COMMIT_API_KEY; -const language = args.language || process.env.AI_COMMIT_LANGUAGE || 'english'; +const language = args.language || process.env.AI_COMMIT_LANGUAGE || "english"; -if (AI_PROVIDER === 'openai' && !apiKey) { - console.error("Please set the OPENAI_API_KEY environment variable."); +if (AI_PROVIDER === "openai" && !apiKey) { + console.error("Please set the AI_COMMIT_API_KEY environment variable."); process.exit(1); } -let template = args.template || process.env.AI_COMMIT_COMMIT_TEMPLATE -const doAddEmoji = args.emoji || process.env.AI_COMMIT_ADD_EMOJI +let template = args.template || process.env.AI_COMMIT_COMMIT_TEMPLATE; +const doAddEmoji = args.emoji || process.env.AI_COMMIT_ADD_EMOJI; -const commitType = args['commit-type']; +const commitType = args["commit-type"]; -const provider = AI_PROVIDER === 'ollama' ? ollama : openai +const provider = PROVIDER_SUPPORT[AI_PROVIDER] || openai; -const customMessageConvention = args['custom-conventions'] +const customMessageConvention = args["custom-conventions"]; const processTemplate = ({ template, commitMessage }) => { - if (!template.includes('COMMIT_MESSAGE')) { - console.log(`Warning: template doesn't include {COMMIT_MESSAGE}`) + if (!template.includes("COMMIT_MESSAGE")) { + console.log(`Warning: template doesn't include {COMMIT_MESSAGE}`); return commitMessage; } - let finalCommitMessage = template.replaceAll("{COMMIT_MESSAGE}", commitMessage); + let finalCommitMessage = template.replaceAll( + "{COMMIT_MESSAGE}", + commitMessage + ); - if (finalCommitMessage.includes('GIT_BRANCH')) { - const currentBranch = execSync("git branch --show-current").toString().replaceAll("\n", ""); + if (finalCommitMessage.includes("GIT_BRANCH")) { + const currentBranch = execSync("git branch --show-current") + .toString() + .replaceAll("\n", ""); - console.log('Using currentBranch: ', currentBranch); + console.log("Using currentBranch: ", currentBranch); - finalCommitMessage = finalCommitMessage.replaceAll("{GIT_BRANCH}", currentBranch) + finalCommitMessage = finalCommitMessage.replaceAll( + "{GIT_BRANCH}", + currentBranch + ); } return finalCommitMessage.trim(); -} +}; const makeCommit = (input) => { console.log("Committing Message... 🚀 "); @@ -59,23 +74,27 @@ const makeCommit = (input) => { console.log("Commit Successful! 🎉"); }; - const processEmoji = (msg, doAddEmoji) => { if (doAddEmoji) { return addGitmojiToCommitMessage(msg); } return msg; -} +}; const getPromptForSingleCommit = (diff) => { - return provider.getPromptForSingleCommit(diff, { commitType, customMessageConvention, language }) + return provider.getPromptForSingleCommit(diff, { + commitType, + customMessageConvention, + language, + }); }; const generateSingleCommit = async (diff) => { - const prompt = getPromptForSingleCommit(diff) - console.log(prompt) - if (!await provider.filterApi({ prompt, filterFee: args['filter-fee'] })) process.exit(1); + const prompt = getPromptForSingleCommit(diff); + console.log(prompt); + if (!(await provider.filterApi({ prompt, filterFee: args["filter-fee"] }))) + process.exit(1); const text = await provider.sendMessage(prompt, { apiKey, model: MODEL }); @@ -85,17 +104,15 @@ const generateSingleCommit = async (diff) => { finalCommitMessage = processTemplate({ template: args.template, commitMessage: finalCommitMessage, - }) + }); console.log( `Proposed Commit With Template:\n------------------------------\n${finalCommitMessage}\n------------------------------` ); } else { - console.log( `Proposed Commit:\n------------------------------\n${finalCommitMessage}\n------------------------------` ); - } if (args.force) { @@ -121,18 +138,35 @@ const generateSingleCommit = async (diff) => { }; const generateListCommits = async (diff, numOptions = 5) => { - const prompt = provider.getPromptForMultipleCommits(diff, { commitType, customMessageConvention, numOptions, language }) - if (!await provider.filterApi({ prompt, filterFee: args['filter-fee'], numCompletion: numOptions })) process.exit(1); + const prompt = provider.getPromptForMultipleCommits(diff, { + commitType, + customMessageConvention, + numOptions, + language, + }); + if ( + !(await provider.filterApi({ + prompt, + filterFee: args["filter-fee"], + numCompletion: numOptions, + })) + ) + process.exit(1); const text = await provider.sendMessage(prompt, { apiKey, model: MODEL }); - let msgs = text.split(";").map((msg) => msg.trim()).map(msg => processEmoji(msg, args.emoji)); + let msgs = text + .split(";") + .map((msg) => msg.trim()) + .map((msg) => processEmoji(msg, args.emoji)); if (args.template) { - msgs = msgs.map(msg => processTemplate({ - template: args.template, - commitMessage: msg, - })) + msgs = msgs.map((msg) => + processTemplate({ + template: args.template, + commitMessage: msg, + }) + ); } // add regenerate option @@ -157,19 +191,23 @@ const generateListCommits = async (diff, numOptions = 5) => { // Add this function after imports const filterLockFiles = (diff) => { - const lines = diff.split('\n'); + const lines = diff.split("\n"); let isLockFile = false; - const filteredLines = lines.filter(line => { - if (line.match(/^diff --git a\/(.*\/)?(yarn\.lock|pnpm-lock\.yaml|package-lock\.json)/)) { + const filteredLines = lines.filter((line) => { + if ( + line.match( + /^diff --git a\/(.*\/)?(yarn\.lock|pnpm-lock\.yaml|package-lock\.json)/ + ) + ) { isLockFile = true; return false; } - if (isLockFile && line.startsWith('diff --git')) { + if (isLockFile && line.startsWith("diff --git")) { isLockFile = false; } return !isLockFile; }); - return filteredLines.join('\n'); + return filteredLines.join("\n"); }; async function generateAICommit() { @@ -188,13 +226,17 @@ async function generateAICommit() { // Check if lock files were changed if (diff !== originalDiff) { - console.log("Changes detected in lock files. These changes will be included in the commit but won't be analyzed for commit message generation."); + console.log( + "Changes detected in lock files. These changes will be included in the commit but won't be analyzed for commit message generation." + ); } // Handle empty diff after filtering if (!diff.trim()) { console.log("No changes to commit except lock files 🙅"); - console.log("Maybe you forgot to add files? Try running git add . and then run this script again."); + console.log( + "Maybe you forgot to add files? Try running git add . and then run this script again." + ); process.exit(1); } diff --git a/openai.js b/openai.js index a98d170..abcfd98 100644 --- a/openai.js +++ b/openai.js @@ -1,8 +1,8 @@ import { ChatGPTAPI } from "chatgpt"; -import { encode } from 'gpt-3-encoder'; +import { encode } from "gpt-3-encoder"; import inquirer from "inquirer"; -import { AI_PROVIDER } from "./config.js" +import { AI_PROVIDER } from "./config.js"; const FEE_PER_1K_TOKENS = 0.02; const MAX_TOKENS = 128000; @@ -10,13 +10,13 @@ const MAX_TOKENS = 128000; const FEE_COMPLETION = 0.001; const openai = { - sendMessage: async (input, {apiKey, model}) => { + sendMessage: async (input, { apiKey, model = "gpt-4o-mini" }) => { console.log("prompting chat gpt..."); console.log("prompt: ", input); const api = new ChatGPTAPI({ apiKey, completionParams: { - model: "gpt-4o-mini", + model, }, }); const { text } = await api.sendMessage(input); @@ -24,26 +24,43 @@ const openai = { return text; }, - getPromptForSingleCommit: (diff, {commitType, customMessageConvention, language}) => { - + getPromptForSingleCommit: ( + diff, + { commitType, customMessageConvention, language } + ) => { return ( `Write a professional git commit message based on the a diff below in ${language} language` + (commitType ? ` with commit type '${commitType}'. ` : ". ") + - `${customMessageConvention ? `Apply the following rules of an JSON formatted object, use key as what has to be changed and value as how it should be changes to your response: ${customMessageConvention}.` : ''}` + + `${ + customMessageConvention + ? `Apply the following rules of an JSON formatted object, use key as what has to be changed and value as how it should be changes to your response: ${customMessageConvention}.` + : "" + }` + "Do not preface the commit with anything, use the present tense, return the full sentence and also commit type" + - `${customMessageConvention ? `. Additionally apply these JSON formatted rules to your response, even though they might be against previous mentioned rules ${customMessageConvention}: ` : ': '}` + - '\n\n'+ + `${ + customMessageConvention + ? `. Additionally apply these JSON formatted rules to your response, even though they might be against previous mentioned rules ${customMessageConvention}: ` + : ": " + }` + + "\n\n" + diff ); }, - getPromptForMultipleCommits: (diff, {commitType, customMessageConvention, numOptions, language}) => { + getPromptForMultipleCommits: ( + diff, + { commitType, customMessageConvention, numOptions, language } + ) => { const prompt = `Write a professional git commit message based on the a diff below in ${language} language` + - (commitType ? ` with commit type '${commitType}'. ` : ". ")+ + (commitType ? ` with commit type '${commitType}'. ` : ". ") + `and make ${numOptions} options that are separated by ";".` + "For each option, use the present tense, return the full sentence and also commit type" + - `${customMessageConvention ? `. Additionally apply these JSON formatted rules to your response, even though they might be against previous mentioned rules ${customMessageConvention}: ` : ': '}` + + `${ + customMessageConvention + ? `. Additionally apply these JSON formatted rules to your response, even though they might be against previous mentioned rules ${customMessageConvention}: ` + : ": " + }` + diff; return prompt; @@ -51,30 +68,31 @@ const openai = { filterApi: async ({ prompt, numCompletion = 1, filterFee }) => { const numTokens = encode(prompt).length; - const fee = numTokens / 1000 * FEE_PER_1K_TOKENS + (FEE_COMPLETION * numCompletion); + const fee = + (numTokens / 1000) * FEE_PER_1K_TOKENS + FEE_COMPLETION * numCompletion; if (numTokens > MAX_TOKENS) { - console.log("The commit diff is too large for the ChatGPT API. Max 4k tokens or ~8k characters. "); - return false; + console.log( + "The commit diff is too large for the ChatGPT API. Max 4k tokens or ~8k characters. " + ); + return false; } if (filterFee) { - console.log(`This will cost you ~$${+fee.toFixed(3)} for using the API.`); - const answer = await inquirer.prompt([ - { - type: "confirm", - name: "continue", - message: "Do you want to continue 💸?", - default: true, - }, - ]); - if (!answer.continue) return false; + console.log(`This will cost you ~$${+fee.toFixed(3)} for using the API.`); + const answer = await inquirer.prompt([ + { + type: "confirm", + name: "continue", + message: "Do you want to continue 💸?", + default: true, + }, + ]); + if (!answer.continue) return false; } return true; -} - - + }, }; export default openai; diff --git a/package-lock.json b/package-lock.json index 390a3ca..a7b7ebb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ai-commit", - "version": "2.1.1", + "version": "2.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ai-commit", - "version": "2.1.1", + "version": "2.2.0", "license": "MIT", "dependencies": { "chatgpt": "^5.0.0", diff --git a/package.json b/package.json index dead2b0..ad749ad 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "publish:major": "npm version major && npm publish --access public", - "ollama": "PROVIDER=ollama node index.js " + "ollama": "PROVIDER=ollama node index.js", + "gemini": "PROVIDER=gemini node index.js" }, "bin": { "ai-commit": "./index.js"