@himamshus06/git-auto 1.0.1 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,39 +23,72 @@ npm install
23
23
  ```
24
24
 
25
25
  ### 3. Install Globally
26
- To use the `git-auto` command anywhere on your system:
26
+ To use the `git auto` command anywhere on your system:
27
27
  ```bash
28
28
  npm install -g @himamshus06/git-auto
29
29
  ```
30
30
 
31
+ To make it work as a git alias (`git ac commit` instead of `git-auto commit`), run:
32
+ ```bash
33
+ git config --global alias.ac "!git-auto"
34
+ ```
35
+
31
36
  ## ⚙️ Configuration
32
37
 
33
- The tool uses the Groq API for fast, free AI generation.
38
+ `git-auto` is provider-agnostic and supports multiple AI backends, including local LLMs.
39
+
40
+ 1. Create a `.env` file in the root directory.
41
+ 2. Choose your preferred provider configuration:
42
+
43
+ ### Option A: Groq (Default - Fast & Free)
44
+ Get a free API key from [Groq Cloud](https://console.groq.com/).
45
+ ```env
46
+ AI_PROVIDER=groq
47
+ AI_API_KEY=your_groq_api_key_here
48
+ ```
49
+
50
+ ### Option B: OpenAI
51
+ ```env
52
+ AI_PROVIDER=openai
53
+ AI_API_KEY=your_openai_api_key_here
54
+ AI_BASE_URL=https://api.openai.com/v1
55
+ AI_MODEL=gpt-4o
56
+ ```
57
+
58
+ ### Option C: Local LLMs (via Ollama)
59
+ Install [Ollama](https://ollama.com/) and run a model (e.g., `ollama run llama3`).
60
+ ```env
61
+ AI_PROVIDER=ollama
62
+ AI_MODEL=llama3
63
+ AI_BASE_URL=http://localhost:11434/v1
64
+ ```
34
65
 
35
- 1. Get a free API key from [Groq Cloud](https://console.groq.com/).
36
- 2. Create a `.env` file in the root directory:
37
- ```env
38
- GROQ_API_KEY=your_api_key_here
39
- ```
66
+ ### Advanced Configuration
67
+ | Variable | Description | Default |
68
+ | :--- | :--- | :--- |
69
+ | `AI_PROVIDER` | The AI backend to use (`groq`, `openai`, `ollama`, `local`) | `groq` |
70
+ | `AI_API_KEY` | API key for cloud providers | (Required for cloud) |
71
+ | `AI_BASE_URL` | API endpoint URL | `https://api.groq.com/openai/v1` |
72
+ | `AI_MODEL` | The specific model ID to use | `qwen/qwen3.8-27b` |
40
73
 
41
74
  ## 🚀 Usage
42
75
 
43
76
  ### Basic Commit
44
77
  Stages all changes and commits with an AI-generated message:
45
78
  ```bash
46
- git-auto commit
79
+ git auto commit
47
80
  ```
48
81
 
49
82
  ### Commit with Custom Message
50
83
  Override the AI and provide your own message:
51
84
  ```bash
52
- git-auto commit -m "feat: add amazing new feature"
85
+ git auto commit -m "feat: add amazing new feature"
53
86
  ```
54
87
 
55
88
  ### Preview Message (Dry Run)
56
89
  See what the AI would generate without actually committing:
57
90
  ```bash
58
- git-auto commit --dry-run
91
+ git auto commit --dry-run
59
92
  ```
60
93
 
61
94
  ## 🛠️ How it Works
package/knowledge.md ADDED
@@ -0,0 +1,53 @@
1
+ # Knowledge Base: git-auto AI Implementation
2
+
3
+ This document serves as a technical reference for the AI architecture of `git-auto`, ensuring future maintainers can extend the provider system without breaking existing functionality.
4
+
5
+ ## Architecture: The Strategy Pattern
6
+
7
+ The AI system is implemented using the **Strategy Pattern**. This decouples the high-level commit message generation logic from the low-level API communication details.
8
+
9
+ ### Component Breakdown
10
+
11
+ #### 1. `AIProvider` (Abstract Base Class)
12
+ - **Purpose**: Defines the contract for all AI providers.
13
+ - **Key Method**: `generate(prompt, options)` - Must be implemented by all subclasses.
14
+
15
+ #### 2. `OpenAICompatibleProvider` (Concrete Strategy)
16
+ - **Purpose**: Handles any AI service that follows the OpenAI chat completion API specification.
17
+ - **Scope**: Covers Groq, OpenAI, LM Studio, and Ollama's `/v1` endpoint.
18
+ - **Implementation**: Uses the `openai` NPM package.
19
+
20
+ #### 3. `getAIProvider()` (Factory)
21
+ - **Purpose**: Instantiates the correct provider based on environment variables.
22
+ - **Logic**:
23
+ - Checks `AI_PROVIDER` to determine the strategy.
24
+ - Configures `baseURL` and `model` with sensible defaults based on the provider.
25
+ - Performs validation (e.g., ensures `AI_API_KEY` exists for cloud providers).
26
+
27
+ #### 4. `generateCommitMessage` (Client Logic)
28
+ - **Purpose**: Orchestrates the actual commit message generation.
29
+ - **Workflow**:
30
+ - Retrieves a provider instance via `getAIProvider()`.
31
+ - Implements a **Map-Reduce** approach for large diffs:
32
+ - **Map**: Splits the diff into chunks ($\approx 8000$ characters) and generates 1-sentence summaries for each.
33
+ - **Reduce**: Combines these summaries into a final, professional commit message.
34
+
35
+ ## Configuration Mapping
36
+
37
+ | Variable | Purpose | Default (Groq) | Default (Ollama) |
38
+ | :--- | :--- | :--- | :--- |
39
+ | `AI_PROVIDER` | Determines the strategy class | `groq` | `ollama` |
40
+ | `AI_BASE_URL` | API endpoint | `https://api.groq.com/openai/v1` | `http://localhost:11434/v1` |
41
+ | `AI_MODEL` | Model Identifier | `qwen/qwen3.8-27b` | `llama3` |
42
+
43
+ ## Future Extension Guide
44
+
45
+ ### How to add a non-OpenAI compatible provider
46
+ If a new provider (e.g., Anthropic Claude) is added that does not use the OpenAI spec:
47
+ 1. Create a new class `ClaudeProvider` extending `AIProvider`.
48
+ 2. Implement the `generate()` method using the provider's specific SDK.
49
+ 3. Add the new provider type to the `getAIProvider()` factory switch statement.
50
+ 4. Update the `.env` documentation in `README.md`.
51
+
52
+ ### How to adjust token limits
53
+ To handle larger diffs or more complex models, modify the `CHUNK_SIZE` constant in `generateCommitMessage`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@himamshus06/git-auto",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/ai.js CHANGED
@@ -1,51 +1,128 @@
1
1
  const OpenAI = require('openai');
2
2
  require('dotenv').config();
3
3
 
4
- const openai = new OpenAI({
5
- apiKey: process.env.GROQ_API_KEY,
6
- baseURL: "https://api.groq.com/openai/v1"
7
- });
4
+ /**
5
+ * Base class for AI Providers.
6
+ * Defines the interface that all providers must implement.
7
+ */
8
+ class AIProvider {
9
+ async generate(prompt, options = {}) {
10
+ throw new Error('Method generate() must be implemented');
11
+ }
12
+ }
13
+
14
+ /**
15
+ * Provider for any service that follows the OpenAI API specification.
16
+ * This covers Groq, OpenAI, LM Studio, and Ollama (/v1).
17
+ */
18
+ class OpenAICompatibleProvider extends AIProvider {
19
+ constructor(config) {
20
+ super();
21
+ this.client = new OpenAI({
22
+ apiKey: config.apiKey || 'ollama',
23
+ baseURL: config.baseURL
24
+ });
25
+ this.model = config.model;
26
+ }
27
+
28
+ async generate(prompt, options = {}) {
29
+ const response = await this.client.chat.completions.create({
30
+ model: this.model,
31
+ messages: [{ role: 'user', content: prompt }],
32
+ temperature: options.temperature ?? 0.2,
33
+ max_tokens: options.maxTokens ?? 100,
34
+ });
35
+ return response.choices[0].message.content.trim();
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Factory to instantiate the appropriate AI provider based on configuration.
41
+ */
42
+ function getAIProvider() {
43
+ const providerType = process.env.AI_PROVIDER || 'groq';
44
+ const apiKey = process.env.AI_API_KEY || process.env.GROQ_API_KEY;
45
+ const baseURL = process.env.AI_BASE_URL || (providerType === 'groq' ? 'https://api.groq.com/openai/v1' : 'http://localhost:11434/v1');
46
+ const model = process.env.AI_MODEL || (providerType === 'groq' ? 'qwen/qwen3.8-27b' : 'llama3');
47
+
48
+ // Validation for cloud providers
49
+ if (!apiKey && providerType !== 'ollama' && providerType !== 'local') {
50
+ throw new Error(`API key is missing for provider "${providerType}". Please set AI_API_KEY in your .env file.`);
51
+ }
52
+
53
+ switch (providerType.toLowerCase()) {
54
+ case 'openai':
55
+ case 'groq':
56
+ case 'ollama':
57
+ case 'local':
58
+ return new OpenAICompatibleProvider({ apiKey, baseURL, model });
59
+ default:
60
+ throw new Error(`Unsupported AI provider: ${providerType}`);
61
+ }
62
+ }
8
63
 
9
64
  /**
10
65
  * Generates a professional commit message based on the provided git diff.
11
- * @param {string} diff - The git diff of staged changes.
12
- * @returns {Promise<string>} - The generated commit message.
13
- * @throws {Error} - If API key is missing or API call fails.
14
66
  */
15
67
  async function generateCommitMessage(diff) {
16
- if (!process.env.GROQ_API_KEY) {
17
- throw new Error('GROQ_API_KEY is missing from .env file.');
68
+ const provider = getAIProvider();
69
+ const CHUNK_SIZE = 8000;
70
+ const isLargeDiff = diff.length > CHUNK_SIZE;
71
+
72
+ if (!isLargeDiff) {
73
+ return await getSingleCommitMessage(provider, diff);
18
74
  }
19
75
 
20
- // TRUNCATION LOGIC:
21
- // Prevent "Request too large" errors by limiting the diff size.
22
- // 10,000 characters is usually enough to understand the changes
23
- // while staying safely under free-tier token limits.
24
- const MAX_DIFF_LENGTH = 10000;
25
- let processedDiff = diff;
26
- if (diff.length > MAX_DIFF_LENGTH) {
27
- processedDiff = diff.substring(0, MAX_DIFF_LENGTH) +
28
- '\n\n... (diff truncated due to size)';
76
+ console.log(`Large diff detected (${diff.length} chars). Using chunked summary approach...`);
77
+
78
+ const chunks = [];
79
+ for (let i = 0; i < diff.length; i += CHUNK_SIZE) {
80
+ chunks.push(diff.substring(i, i + CHUNK_SIZE));
29
81
  }
30
82
 
83
+ const summaryPromises = chunks.map(async (chunk, index) => {
84
+ const prompt = `Analyze this portion (${index + 1}/${chunks.length}) of a git diff and provide a 1-sentence summary of the changes.\nDiff snippet:\n${chunk}`;
85
+ try {
86
+ return await provider.generate(prompt);
87
+ } catch (e) {
88
+ return `Error summarizing chunk ${index + 1}: ${e.message}`;
89
+ }
90
+ });
91
+
92
+ const summaries = await Promise.all(summaryPromises);
93
+ const combinedSummaries = summaries.join('\n');
94
+
95
+ const finalPrompt = `
96
+ Based on the following summaries of changes across multiple files, write one professional, concise commit message.
97
+ Follow the Conventional Commits specification (e.g., feat: ..., fix: ..., chore: ..., docs: ..., style: ..., refactor: ..., perf: ..., test: ...).
98
+ Only return the commit message string itself, without any quotes or explanation.
99
+
100
+ Summaries:
101
+ ${combinedSummaries}
102
+ `.trim();
103
+
104
+ try {
105
+ return await provider.generate(finalPrompt);
106
+ } catch (error) {
107
+ throw new Error(`Final reduction failed: ${error.message}`);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Helper to generate a commit message from a single diff.
113
+ */
114
+ async function getSingleCommitMessage(provider, diff) {
31
115
  const prompt = `
32
116
  Analyze the following git diff and write a professional, concise commit message.
33
117
  Follow the Conventional Commits specification (e.g., feat: ..., fix: ..., chore: ..., docs: ..., style: ..., refactor: ..., perf: ..., test: ...).
34
118
  Only return the commit message string itself, without any quotes or explanation.
35
119
 
36
120
  Diff:
37
- ${processedDiff}
121
+ ${diff}
38
122
  `.trim();
39
123
 
40
124
  try {
41
- const response = await openai.chat.completions.create({
42
- model: 'qwen/qwen3.8-27b',
43
- messages: [{ role: 'user', content: prompt }],
44
- temperature: 0.2,
45
- max_tokens: 100,
46
- });
47
-
48
- return response.choices[0].message.content.trim();
125
+ return await provider.generate(prompt);
49
126
  } catch (error) {
50
127
  throw new Error(`AI generation failed: ${error.message}`);
51
128
  }
package/src/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  const { Command } = require('commander');
3
+ const readline = require('node:readline/promises');
4
+ const { stdin: input, stdout: output } = require('node:process');
3
5
  const git = require('./git');
4
6
  const ai = require('./ai');
5
7
  require('dotenv').config();
@@ -17,6 +19,7 @@ program
17
19
  .option('-m, --message <message>', 'override the AI-generated commit message')
18
20
  .option('--dry-run', 'preview the generated message without committing')
19
21
  .action(async (options) => {
22
+ const rl = readline.createInterface({ input, output });
20
23
  try {
21
24
  if (!(await git.isGitRepo())) {
22
25
  console.error('Error: Current directory is not a git repository.');
@@ -29,6 +32,7 @@ program
29
32
  const diff = await git.getStagedDiff();
30
33
  if (!diff) {
31
34
  console.log('No changes to commit.');
35
+ rl.close();
32
36
  return;
33
37
  }
34
38
 
@@ -39,21 +43,39 @@ program
39
43
  } else {
40
44
  console.log('Generating AI commit message...');
41
45
  commitMessage = await ai.generateCommitMessage(diff);
42
- console.log(`Generated message: ${commitMessage}`);
46
+ console.log(`\nGenerated message: ${commitMessage}\n`);
47
+
48
+ if (!options.dryRun) {
49
+ const choice = await rl.question('(A)ccept, (E)dit, or (D)ecline? [a/e/d]: ');
50
+ const action = choice.toLowerCase().trim();
51
+
52
+ if (action === 'd') {
53
+ console.log('Commit cancelled by user.');
54
+ rl.close();
55
+ return;
56
+ } else if (action === 'e') {
57
+ const newMessage = await rl.question('Enter new commit message: ');
58
+ commitMessage = newMessage.trim() || commitMessage;
59
+ console.log(`Updated message: ${commitMessage}`);
60
+ } else {
61
+ // Default to accept
62
+ }
63
+ }
43
64
  }
44
65
 
45
66
  if (options.dryRun) {
46
67
  console.log('Dry run enabled. Skipping commit.');
47
- return;
68
+ } else {
69
+ console.log('Committing changes...');
70
+ await git.commit(commitMessage);
71
+ console.log('Successfully committed changes!');
48
72
  }
49
73
 
50
- console.log('Committing changes...');
51
- await git.commit(commitMessage);
52
- console.log('Successfully committed changes!');
53
-
54
74
  } catch (error) {
55
75
  console.error(`Error: ${error.message}`);
56
76
  process.exit(1);
77
+ } finally {
78
+ rl.close();
57
79
  }
58
80
  });
59
81