@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 +43 -10
- package/knowledge.md +53 -0
- package/package.json +1 -1
- package/src/ai.js +104 -27
- package/src/index.js +28 -6
package/README.md
CHANGED
|
@@ -23,39 +23,72 @@ npm install
|
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
### 3. Install Globally
|
|
26
|
-
To use the `git
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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
|
|
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
|
|
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
package/src/ai.js
CHANGED
|
@@ -1,51 +1,128 @@
|
|
|
1
1
|
const OpenAI = require('openai');
|
|
2
2
|
require('dotenv').config();
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
${
|
|
121
|
+
${diff}
|
|
38
122
|
`.trim();
|
|
39
123
|
|
|
40
124
|
try {
|
|
41
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|