gims 0.5.4 → 0.6.2

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.
@@ -0,0 +1,286 @@
1
+ const { OpenAI } = require('openai');
2
+ const { GoogleGenAI } = require('@google/genai');
3
+ const { Progress } = require('../utils/progress');
4
+ const { color } = require('../utils/colors');
5
+
6
+ /**
7
+ * Enhanced AI provider management with caching and fallbacks
8
+ */
9
+ class AIProviderManager {
10
+ constructor(config = {}) {
11
+ this.config = config;
12
+ this.cache = new Map();
13
+ this.maxCacheSize = 100;
14
+ }
15
+
16
+ resolveProvider(preference = 'auto') {
17
+ if (preference === 'none') return 'none';
18
+ if (preference === 'openai') return process.env.OPENAI_API_KEY ? 'openai' : 'none';
19
+ if (preference === 'gemini') return process.env.GEMINI_API_KEY ? 'gemini' : 'none';
20
+ if (preference === 'groq') return process.env.GROQ_API_KEY ? 'groq' : 'none';
21
+
22
+ // Auto-detection with preference order (Gemini first - fastest and cheapest)
23
+ if (process.env.GEMINI_API_KEY) return 'gemini';
24
+ if (process.env.OPENAI_API_KEY) return 'openai';
25
+ if (process.env.GROQ_API_KEY) return 'groq';
26
+ return 'none';
27
+ }
28
+
29
+ getDefaultModel(provider) {
30
+ const defaults = {
31
+ 'gemini': 'gemini-2.5-flash', // Latest and fastest
32
+ 'openai': 'gpt-5', // Latest GPT model
33
+ 'groq': 'groq/compound' // Latest Groq model
34
+ };
35
+ return defaults[provider] || '';
36
+ }
37
+
38
+ getCacheKey(prompt, options) {
39
+ const crypto = require('crypto');
40
+ const key = JSON.stringify({ prompt: prompt.substring(0, 1000), options });
41
+ return crypto.createHash('md5').update(key).digest('hex');
42
+ }
43
+
44
+ getFromCache(cacheKey) {
45
+ if (!this.config.cacheEnabled) return null;
46
+ return this.cache.get(cacheKey);
47
+ }
48
+
49
+ setCache(cacheKey, result) {
50
+ if (!this.config.cacheEnabled) return;
51
+
52
+ if (this.cache.size >= this.maxCacheSize) {
53
+ const firstKey = this.cache.keys().next().value;
54
+ this.cache.delete(firstKey);
55
+ }
56
+
57
+ this.cache.set(cacheKey, {
58
+ result,
59
+ timestamp: Date.now()
60
+ });
61
+ }
62
+
63
+ async generateWithProvider(provider, prompt, options = {}) {
64
+ const { model = '', temperature = 0.3, maxTokens = 200 } = options;
65
+
66
+ try {
67
+ switch (provider) {
68
+ case 'gemini':
69
+ return await this.generateWithGemini(prompt, model || 'gemini-2.0-flash', options);
70
+ case 'openai':
71
+ return await this.generateWithOpenAI(prompt, model || 'gpt-4o-mini', options);
72
+ case 'groq':
73
+ return await this.generateWithGroq(prompt, model || 'llama-3.1-8b-instant', options);
74
+ default:
75
+ throw new Error(`Unknown provider: ${provider}`);
76
+ }
77
+ } catch (error) {
78
+ throw new Error(`${provider} generation failed: ${error.message}`);
79
+ }
80
+ }
81
+
82
+ async generateWithGemini(prompt, model, options) {
83
+ const genai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
84
+ const actualModel = model || this.getDefaultModel('gemini');
85
+ const response = await genai.models.generateContent({
86
+ model: actualModel,
87
+ contents: prompt,
88
+ });
89
+ return (await response.response.text()).trim();
90
+ }
91
+
92
+ async generateWithOpenAI(prompt, model, options) {
93
+ const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
94
+ const actualModel = model || this.getDefaultModel('openai');
95
+ const response = await openai.chat.completions.create({
96
+ model: actualModel,
97
+ messages: [{ role: 'user', content: prompt }],
98
+ temperature: options.temperature || 0.3,
99
+ max_tokens: options.maxTokens || 200,
100
+ });
101
+ return (response.choices[0]?.message?.content || '').trim();
102
+ }
103
+
104
+ async generateWithGroq(prompt, model, options) {
105
+ const groq = new OpenAI({
106
+ apiKey: process.env.GROQ_API_KEY,
107
+ baseURL: process.env.GROQ_BASE_URL || 'https://api.groq.com/openai/v1'
108
+ });
109
+ const actualModel = model || this.getDefaultModel('groq');
110
+ const response = await groq.chat.completions.create({
111
+ model: actualModel,
112
+ messages: [{ role: 'user', content: prompt }],
113
+ temperature: options.temperature || 0.3,
114
+ max_tokens: options.maxTokens || 200,
115
+ });
116
+ return (response.choices[0]?.message?.content || '').trim();
117
+ }
118
+
119
+ async generateCommitMessage(diff, options = {}) {
120
+ const {
121
+ provider: preferredProvider = 'auto',
122
+ conventional = false,
123
+ body = false,
124
+ verbose = false
125
+ } = options;
126
+
127
+ // Check cache first
128
+ const cacheKey = this.getCacheKey(diff, { conventional, body });
129
+ const cached = this.getFromCache(cacheKey);
130
+ if (cached && Date.now() - cached.timestamp < 3600000) { // 1 hour cache
131
+ if (verbose) Progress.info('Using cached result');
132
+ return cached.result;
133
+ }
134
+
135
+ const providerChain = this.buildProviderChain(preferredProvider);
136
+
137
+ for (const provider of providerChain) {
138
+ try {
139
+ if (verbose) Progress.info(`Trying provider: ${provider}`);
140
+
141
+ if (provider === 'local') {
142
+ const result = await this.generateLocalHeuristic(diff, options);
143
+ this.setCache(cacheKey, result);
144
+ return result;
145
+ }
146
+
147
+ const prompt = this.buildPrompt(diff, { conventional, body });
148
+ const result = await this.generateWithProvider(provider, prompt, options);
149
+ const cleaned = this.cleanCommitMessage(result, { body });
150
+
151
+ this.setCache(cacheKey, cleaned);
152
+ return cleaned;
153
+
154
+ } catch (error) {
155
+ if (verbose) Progress.warning(`${provider} failed: ${error.message}`);
156
+ continue;
157
+ }
158
+ }
159
+
160
+ // Final fallback
161
+ const fallback = 'Update project files';
162
+ this.setCache(cacheKey, fallback);
163
+ return fallback;
164
+ }
165
+
166
+ buildProviderChain(preferred) {
167
+ const available = [];
168
+
169
+ if (preferred !== 'auto' && preferred !== 'none') {
170
+ const resolved = this.resolveProvider(preferred);
171
+ if (resolved !== 'none') available.push(resolved);
172
+ } else if (preferred === 'auto') {
173
+ if (process.env.GEMINI_API_KEY) available.push('gemini');
174
+ if (process.env.OPENAI_API_KEY) available.push('openai');
175
+ if (process.env.GROQ_API_KEY) available.push('groq');
176
+ }
177
+
178
+ available.push('local');
179
+ return [...new Set(available)];
180
+ }
181
+
182
+ buildPrompt(diff, options) {
183
+ const { conventional, body } = options;
184
+
185
+ const style = conventional
186
+ ? 'Use Conventional Commits format (e.g., feat:, fix:, chore:) for the subject.'
187
+ : 'Subject must be a single short line.';
188
+
189
+ const bodyInstr = body
190
+ ? 'Provide a short subject line followed by an optional body separated by a blank line.'
191
+ : 'Return only a short subject line without extra quotes.';
192
+
193
+ return `Write a concise git commit message for these changes:\n${diff}\n\n${style} ${bodyInstr}`;
194
+ }
195
+
196
+ cleanCommitMessage(message, options = {}) {
197
+ if (!message) return 'Update project code';
198
+
199
+ // Remove markdown formatting
200
+ let cleaned = message
201
+ .replace(/```[\s\S]*?```/g, '')
202
+ .replace(/`([^`]+)`/g, '$1')
203
+ .replace(/^\s*[-*+]\s*/gm, '')
204
+ .replace(/^\s*\d+\.\s*/gm, '')
205
+ .replace(/^\s*#+\s*/gm, '')
206
+ .replace(/\*\*(.*?)\*\*/g, '$1')
207
+ .replace(/\*(.*?)\*/g, '$1')
208
+ .replace(/[\u{1F300}-\u{1FAFF}]/gu, '')
209
+ .replace(/[\t\r]+/g, ' ')
210
+ .trim();
211
+
212
+ const lines = cleaned.split('\n').map(l => l.trim()).filter(Boolean);
213
+ let subject = (lines[0] || '').replace(/\s{2,}/g, ' ').replace(/[\s:,.!;]+$/g, '').trim();
214
+
215
+ if (subject.length === 0) subject = 'Update project code';
216
+ if (subject.length > 72) subject = subject.substring(0, 69) + '...';
217
+
218
+ if (!options.body) return subject;
219
+
220
+ const bodyLines = lines.slice(1).filter(l => l.length > 0);
221
+ const bodyText = bodyLines.join('\n').trim();
222
+ return bodyText ? `${subject}\n\n${bodyText}` : subject;
223
+ }
224
+
225
+ async generateLocalHeuristic(diff, options) {
226
+ // This would need access to git status - simplified version
227
+ const { conventional = false } = options;
228
+
229
+ // Analyze diff for patterns
230
+ const lines = diff.split('\n');
231
+ const additions = lines.filter(l => l.startsWith('+')).length;
232
+ const deletions = lines.filter(l => l.startsWith('-')).length;
233
+ const files = (diff.match(/diff --git/g) || []).length;
234
+
235
+ let type = 'chore';
236
+ let subject = 'update files';
237
+
238
+ if (additions > deletions * 2) {
239
+ type = 'feat';
240
+ subject = files === 1 ? 'add new functionality' : `add features to ${files} files`;
241
+ } else if (deletions > additions * 2) {
242
+ type = 'chore';
243
+ subject = files === 1 ? 'remove unused code' : `clean up ${files} files`;
244
+ } else if (diff.includes('test') || diff.includes('spec')) {
245
+ type = 'test';
246
+ subject = 'update tests';
247
+ } else if (diff.includes('README') || diff.includes('doc')) {
248
+ type = 'docs';
249
+ subject = 'update documentation';
250
+ }
251
+
252
+ return conventional ? `${type}: ${subject}` : subject.charAt(0).toUpperCase() + subject.slice(1);
253
+ }
254
+
255
+ async generateMultipleSuggestions(diff, options = {}, count = 3) {
256
+ const suggestions = [];
257
+ const baseOptions = { ...options };
258
+
259
+ // Generate different styles
260
+ const variants = [
261
+ { ...baseOptions, conventional: false },
262
+ { ...baseOptions, conventional: true },
263
+ { ...baseOptions, conventional: true, body: true }
264
+ ];
265
+
266
+ for (let i = 0; i < Math.min(count, variants.length); i++) {
267
+ try {
268
+ const suggestion = await this.generateCommitMessage(diff, variants[i]);
269
+ if (!suggestions.includes(suggestion)) {
270
+ suggestions.push(suggestion);
271
+ }
272
+ } catch (error) {
273
+ // Skip failed generations
274
+ }
275
+ }
276
+
277
+ // Ensure we have at least one suggestion
278
+ if (suggestions.length === 0) {
279
+ suggestions.push('Update project files');
280
+ }
281
+
282
+ return suggestions;
283
+ }
284
+ }
285
+
286
+ module.exports = { AIProviderManager };
@@ -0,0 +1,241 @@
1
+ const readline = require('readline');
2
+ const { color } = require('../utils/colors');
3
+ const { Progress } = require('../utils/progress');
4
+
5
+ /**
6
+ * Interactive commit wizard and user input utilities
7
+ */
8
+ class InteractiveCommands {
9
+ constructor(git, aiProvider, analyzer) {
10
+ this.git = git;
11
+ this.aiProvider = aiProvider;
12
+ this.analyzer = analyzer;
13
+ }
14
+
15
+ async promptUser(question, defaultValue = '') {
16
+ const rl = readline.createInterface({
17
+ input: process.stdin,
18
+ output: process.stdout
19
+ });
20
+
21
+ return new Promise(resolve => {
22
+ rl.question(question, answer => {
23
+ rl.close();
24
+ resolve(answer.trim() || defaultValue);
25
+ });
26
+ });
27
+ }
28
+
29
+ async selectFromList(items, prompt = 'Select an option:', allowCustom = false) {
30
+ console.log(`\n${color.bold(prompt)}`);
31
+
32
+ items.forEach((item, index) => {
33
+ console.log(` ${color.cyan((index + 1).toString())}. ${item}`);
34
+ });
35
+
36
+ if (allowCustom) {
37
+ console.log(` ${color.cyan('c')}. Custom message`);
38
+ }
39
+
40
+ const maxChoice = items.length;
41
+ const validChoices = Array.from({length: maxChoice}, (_, i) => (i + 1).toString());
42
+ if (allowCustom) validChoices.push('c');
43
+
44
+ let choice;
45
+ do {
46
+ choice = await this.promptUser(`\nChoice (1-${maxChoice}${allowCustom ? ', c' : ''}): `);
47
+ } while (!validChoices.includes(choice));
48
+
49
+ if (choice === 'c') {
50
+ return await this.promptUser('Enter custom message: ');
51
+ }
52
+
53
+ return items[parseInt(choice) - 1];
54
+ }
55
+
56
+ async runInteractiveCommit(options = {}) {
57
+ try {
58
+ console.log(color.bold('\nšŸŽÆ Interactive Commit Wizard\n'));
59
+
60
+ // Step 1: Check for changes
61
+ Progress.step(1, 4, 'Analyzing changes...');
62
+ const status = await this.git.status();
63
+
64
+ if (status.files.length === 0) {
65
+ Progress.warning('No changes detected');
66
+ return;
67
+ }
68
+
69
+ // Step 2: Show status and get staging preference
70
+ Progress.step(2, 4, 'Reviewing file changes...');
71
+ const enhancedStatus = await this.analyzer.getEnhancedStatus();
72
+ console.log(this.analyzer.formatStatusOutput(enhancedStatus));
73
+
74
+ // Ask about staging
75
+ let shouldStage = false;
76
+ if (status.staged.length === 0) {
77
+ const stageChoice = await this.promptUser(
78
+ 'No files are staged. Stage all changes? (y/n) [y]: ',
79
+ 'y'
80
+ );
81
+ shouldStage = stageChoice.toLowerCase() === 'y';
82
+
83
+ if (shouldStage) {
84
+ await this.git.add('.');
85
+ Progress.success('All changes staged');
86
+ } else {
87
+ console.log('You can manually stage files with: git add <file>');
88
+ return;
89
+ }
90
+ }
91
+
92
+ // Step 3: Generate commit message suggestions
93
+ Progress.step(3, 4, 'Generating AI suggestions...');
94
+ Progress.start('šŸ¤– AI is analyzing your changes');
95
+
96
+ const diff = await this.git.diff(['--cached', '--no-ext-diff']);
97
+ if (!diff.trim()) {
98
+ Progress.stop(color.yellow('No staged changes to commit'));
99
+ return;
100
+ }
101
+
102
+ const suggestions = await this.aiProvider.generateMultipleSuggestions(diff, options, 3);
103
+ Progress.stop(color.green('āœ“ Generated suggestions'));
104
+
105
+ // Step 4: Let user choose
106
+ Progress.step(4, 4, 'Select commit message...');
107
+ const selectedMessage = await this.selectFromList(
108
+ suggestions,
109
+ 'Choose a commit message:',
110
+ true
111
+ );
112
+
113
+ // Confirm and commit
114
+ console.log(`\nSelected message: ${color.green(selectedMessage)}`);
115
+ const confirm = await this.promptUser('Proceed with commit? (y/n) [y]: ', 'y');
116
+
117
+ if (confirm.toLowerCase() === 'y') {
118
+ if (options.dryRun) {
119
+ console.log(color.yellow('[dry-run] Would commit with message:'));
120
+ console.log(selectedMessage);
121
+ } else {
122
+ await this.git.commit(selectedMessage);
123
+ Progress.success(`Committed: "${selectedMessage}"`);
124
+
125
+ // Ask about pushing
126
+ const pushChoice = await this.promptUser('Push to remote? (y/n) [n]: ', 'n');
127
+ if (pushChoice.toLowerCase() === 'y') {
128
+ try {
129
+ await this.git.push();
130
+ Progress.success('Pushed to remote');
131
+ } catch (error) {
132
+ Progress.error(`Push failed: ${error.message}`);
133
+ }
134
+ }
135
+ }
136
+ } else {
137
+ console.log('Commit cancelled');
138
+ }
139
+
140
+ } catch (error) {
141
+ Progress.error(`Interactive commit failed: ${error.message}`);
142
+ throw error;
143
+ }
144
+ }
145
+
146
+ async runQuickCommit(message, options = {}) {
147
+ try {
148
+ const status = await this.git.status();
149
+
150
+ if (status.files.length === 0) {
151
+ console.log('No changes to commit');
152
+ return;
153
+ }
154
+
155
+ // Auto-stage if nothing is staged
156
+ if (status.staged.length === 0) {
157
+ console.log(color.yellow('Auto-staging all changes...'));
158
+ await this.git.add('.');
159
+ }
160
+
161
+ if (options.dryRun) {
162
+ console.log(color.yellow('[dry-run] Would commit with message:'));
163
+ console.log(message);
164
+ return;
165
+ }
166
+
167
+ await this.git.commit(message);
168
+ Progress.success(`Committed: "${message}"`);
169
+
170
+ if (options.push) {
171
+ try {
172
+ await this.git.push();
173
+ Progress.success('Pushed to remote');
174
+ } catch (error) {
175
+ Progress.warning(`Push failed: ${error.message}`);
176
+ }
177
+ }
178
+
179
+ } catch (error) {
180
+ Progress.error(`Quick commit failed: ${error.message}`);
181
+ throw error;
182
+ }
183
+ }
184
+
185
+ async showCommitPreview(options = {}) {
186
+ try {
187
+ const status = await this.git.status();
188
+
189
+ if (status.files.length === 0) {
190
+ console.log('No changes to preview');
191
+ return;
192
+ }
193
+
194
+ // Show what would be committed
195
+ const diff = await this.git.diff(['--cached', '--no-ext-diff']);
196
+ if (!diff.trim()) {
197
+ console.log(color.yellow('No staged changes. Use --all to stage everything.'));
198
+ return;
199
+ }
200
+
201
+ console.log(color.bold('\nšŸ“‹ Commit Preview\n'));
202
+
203
+ // Show file summary
204
+ const complexity = await this.analyzer.getChangeComplexity(diff);
205
+ console.log(`Complexity: ${color.cyan(complexity.complexity)}`);
206
+ console.log(`Files: ${complexity.files}, +${complexity.additions} -${complexity.deletions}`);
207
+
208
+ // Generate and show AI suggestion
209
+ Progress.start('šŸ¤– Generating commit message');
210
+ const suggestion = await this.aiProvider.generateCommitMessage(diff, options);
211
+ Progress.stop('');
212
+
213
+ console.log(`\nSuggested message: ${color.green(suggestion)}`);
214
+
215
+ // Show diff summary (first few lines)
216
+ console.log(`\n${color.bold('Changes:')}`);
217
+ const diffLines = diff.split('\n').slice(0, 20);
218
+ diffLines.forEach(line => {
219
+ if (line.startsWith('+')) {
220
+ console.log(color.green(line));
221
+ } else if (line.startsWith('-')) {
222
+ console.log(color.red(line));
223
+ } else if (line.startsWith('@@')) {
224
+ console.log(color.cyan(line));
225
+ } else {
226
+ console.log(color.dim(line));
227
+ }
228
+ });
229
+
230
+ if (diff.split('\n').length > 20) {
231
+ console.log(color.dim('... (truncated)'));
232
+ }
233
+
234
+ } catch (error) {
235
+ Progress.error(`Preview failed: ${error.message}`);
236
+ throw error;
237
+ }
238
+ }
239
+ }
240
+
241
+ module.exports = { InteractiveCommands };