gims 0.6.7 → 0.8.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.
@@ -18,7 +18,7 @@ class AIProviderManager {
18
18
  if (preference === 'openai') return process.env.OPENAI_API_KEY ? 'openai' : 'none';
19
19
  if (preference === 'gemini') return process.env.GEMINI_API_KEY ? 'gemini' : 'none';
20
20
  if (preference === 'groq') return process.env.GROQ_API_KEY ? 'groq' : 'none';
21
-
21
+
22
22
  // Auto-detection with preference order (Gemini first - fastest and cheapest)
23
23
  if (process.env.GEMINI_API_KEY) return 'gemini';
24
24
  if (process.env.OPENAI_API_KEY) return 'openai';
@@ -48,12 +48,12 @@ class AIProviderManager {
48
48
 
49
49
  setCache(cacheKey, result, usedLocal = false) {
50
50
  if (!this.config.cacheEnabled) return;
51
-
51
+
52
52
  if (this.cache.size >= this.maxCacheSize) {
53
53
  const firstKey = this.cache.keys().next().value;
54
54
  this.cache.delete(firstKey);
55
55
  }
56
-
56
+
57
57
  this.cache.set(cacheKey, {
58
58
  result,
59
59
  usedLocal,
@@ -103,9 +103,9 @@ class AIProviderManager {
103
103
  }
104
104
 
105
105
  async generateWithGroq(prompt, model, options) {
106
- const groq = new OpenAI({
107
- apiKey: process.env.GROQ_API_KEY,
108
- baseURL: process.env.GROQ_BASE_URL || 'https://api.groq.com/openai/v1'
106
+ const groq = new OpenAI({
107
+ apiKey: process.env.GROQ_API_KEY,
108
+ baseURL: process.env.GROQ_BASE_URL || 'https://api.groq.com/openai/v1'
109
109
  });
110
110
  const actualModel = model || this.getDefaultModel('groq');
111
111
  const response = await groq.chat.completions.create({
@@ -118,11 +118,11 @@ class AIProviderManager {
118
118
  }
119
119
 
120
120
  async generateCommitMessage(diff, options = {}) {
121
- const {
122
- provider: preferredProvider = 'auto',
123
- conventional = false,
121
+ const {
122
+ provider: preferredProvider = 'auto',
123
+ conventional = false,
124
124
  body = false,
125
- verbose = false
125
+ verbose = false
126
126
  } = options;
127
127
 
128
128
  // Check cache first
@@ -134,11 +134,11 @@ class AIProviderManager {
134
134
  }
135
135
 
136
136
  const providerChain = this.buildProviderChain(preferredProvider);
137
-
137
+
138
138
  for (const provider of providerChain) {
139
139
  try {
140
140
  if (verbose) Progress.info(`Trying provider: ${provider}`);
141
-
141
+
142
142
  if (provider === 'local') {
143
143
  const result = await this.generateLocalHeuristic(diff, options);
144
144
  this.setCache(cacheKey, result, true);
@@ -148,10 +148,10 @@ class AIProviderManager {
148
148
  const prompt = this.buildPrompt(diff, { conventional, body });
149
149
  const result = await this.generateWithProvider(provider, prompt, options);
150
150
  const cleaned = this.cleanCommitMessage(result, { body });
151
-
151
+
152
152
  this.setCache(cacheKey, cleaned, false);
153
153
  return { message: cleaned, usedLocal: false };
154
-
154
+
155
155
  } catch (error) {
156
156
  if (verbose) Progress.warning(`${provider} failed: ${error.message}`);
157
157
  continue;
@@ -166,7 +166,7 @@ class AIProviderManager {
166
166
 
167
167
  buildProviderChain(preferred) {
168
168
  const available = [];
169
-
169
+
170
170
  if (preferred !== 'auto' && preferred !== 'none') {
171
171
  const resolved = this.resolveProvider(preferred);
172
172
  if (resolved !== 'none') available.push(resolved);
@@ -175,28 +175,28 @@ class AIProviderManager {
175
175
  if (process.env.OPENAI_API_KEY) available.push('openai');
176
176
  if (process.env.GROQ_API_KEY) available.push('groq');
177
177
  }
178
-
178
+
179
179
  available.push('local');
180
180
  return [...new Set(available)];
181
181
  }
182
182
 
183
183
  buildPrompt(diff, options) {
184
184
  const { conventional, body } = options;
185
-
186
- const style = conventional
185
+
186
+ const style = conventional
187
187
  ? 'Use Conventional Commits format (e.g., feat:, fix:, chore:) for the subject.'
188
188
  : 'Subject must be a single short line.';
189
-
190
- const bodyInstr = body
189
+
190
+ const bodyInstr = body
191
191
  ? 'Provide a short subject line followed by an optional body separated by a blank line.'
192
192
  : 'Return only a short subject line without extra quotes.';
193
-
193
+
194
194
  return `Write a concise git commit message for these changes:\n${diff}\n\n${style} ${bodyInstr}`;
195
195
  }
196
196
 
197
197
  cleanCommitMessage(message, options = {}) {
198
198
  if (!message) return 'Update project code';
199
-
199
+
200
200
  // Remove markdown formatting
201
201
  let cleaned = message
202
202
  .replace(/```[\s\S]*?```/g, '')
@@ -212,7 +212,7 @@ class AIProviderManager {
212
212
 
213
213
  const lines = cleaned.split('\n').map(l => l.trim()).filter(Boolean);
214
214
  let subject = (lines[0] || '').replace(/\s{2,}/g, ' ').replace(/[\s:,.!;]+$/g, '').trim();
215
-
215
+
216
216
  if (subject.length === 0) subject = 'Update project code';
217
217
  // No length restriction - allow AI to generate full commit messages
218
218
 
@@ -226,16 +226,16 @@ class AIProviderManager {
226
226
  async generateLocalHeuristic(diff, options) {
227
227
  // This would need access to git status - simplified version
228
228
  const { conventional = false } = options;
229
-
229
+
230
230
  // Analyze diff for patterns
231
231
  const lines = diff.split('\n');
232
232
  const additions = lines.filter(l => l.startsWith('+')).length;
233
233
  const deletions = lines.filter(l => l.startsWith('-')).length;
234
234
  const files = (diff.match(/diff --git/g) || []).length;
235
-
235
+
236
236
  let type = 'chore';
237
237
  let subject = 'update files';
238
-
238
+
239
239
  if (additions > deletions * 2) {
240
240
  type = 'feat';
241
241
  subject = files === 1 ? 'add new functionality' : `add features to ${files} files`;
@@ -249,37 +249,39 @@ class AIProviderManager {
249
249
  type = 'docs';
250
250
  subject = 'update documentation';
251
251
  }
252
-
252
+
253
253
  return conventional ? `${type}: ${subject}` : subject.charAt(0).toUpperCase() + subject.slice(1);
254
254
  }
255
255
 
256
256
  async generateMultipleSuggestions(diff, options = {}, count = 3) {
257
257
  const suggestions = [];
258
258
  const baseOptions = { ...options };
259
-
259
+
260
260
  // Generate different styles
261
261
  const variants = [
262
262
  { ...baseOptions, conventional: false },
263
263
  { ...baseOptions, conventional: true },
264
264
  { ...baseOptions, conventional: true, body: true }
265
265
  ];
266
-
266
+
267
267
  for (let i = 0; i < Math.min(count, variants.length); i++) {
268
268
  try {
269
- const suggestion = await this.generateCommitMessage(diff, variants[i]);
270
- if (!suggestions.includes(suggestion)) {
271
- suggestions.push(suggestion);
269
+ const result = await this.generateCommitMessage(diff, variants[i]);
270
+ // Extract message string from result object
271
+ const message = result.message || result;
272
+ if (message && !suggestions.includes(message)) {
273
+ suggestions.push(message);
272
274
  }
273
275
  } catch (error) {
274
276
  // Skip failed generations
275
277
  }
276
278
  }
277
-
279
+
278
280
  // Ensure we have at least one suggestion
279
281
  if (suggestions.length === 0) {
280
282
  suggestions.push('Update project files');
281
283
  }
282
-
284
+
283
285
  return suggestions;
284
286
  }
285
287
  }
@@ -1,4 +1,5 @@
1
1
  const { color } = require('../utils/colors');
2
+ const { Intelligence } = require('../utils/intelligence');
2
3
 
3
4
  /**
4
5
  * Enhanced git analysis and insights
@@ -6,17 +7,22 @@ const { color } = require('../utils/colors');
6
7
  class GitAnalyzer {
7
8
  constructor(git) {
8
9
  this.git = git;
10
+ this.intelligence = new Intelligence(git);
9
11
  }
10
12
 
11
13
  async getEnhancedStatus() {
12
14
  try {
13
15
  const status = await this.git.status();
14
16
  const insights = await this.generateStatusInsights(status);
15
-
17
+ const sessionStats = await this.intelligence.getSessionStats();
18
+ const branchContext = await this.intelligence.detectBranchContext();
19
+
16
20
  return {
17
21
  ...status,
18
22
  insights,
19
- summary: this.generateStatusSummary(status)
23
+ summary: this.generateStatusSummary(status),
24
+ sessionStats,
25
+ branchContext,
20
26
  };
21
27
  } catch (error) {
22
28
  throw new Error(`Failed to get git status: ${error.message}`);
@@ -32,47 +38,48 @@ class GitAnalyzer {
32
38
  const untracked = Array.isArray(status.not_added) ? status.not_added : [];
33
39
 
34
40
  if (files.length === 0) return 'Working tree clean';
35
-
41
+
36
42
  const parts = [];
37
43
  if (staged.length > 0) parts.push(`${staged.length} staged`);
38
44
  if (modified.length > 0) parts.push(`${modified.length} modified`);
39
45
  if (created.length > 0) parts.push(`${created.length} new`);
40
46
  if (deleted.length > 0) parts.push(`${deleted.length} deleted`);
41
47
  if (untracked.length > 0) parts.push(`${untracked.length} untracked`);
42
-
48
+
43
49
  return parts.join(', ');
44
50
  }
45
51
 
46
52
  async generateStatusInsights(status) {
47
53
  const insights = [];
48
-
54
+
49
55
  // Ensure arrays exist and have proper methods
50
56
  const modified = Array.isArray(status.modified) ? status.modified : [];
51
57
  const created = Array.isArray(status.created) ? status.created : [];
52
58
  const deleted = Array.isArray(status.deleted) ? status.deleted : [];
53
59
  const files = Array.isArray(status.files) ? status.files : [];
54
-
60
+ const staged = Array.isArray(status.staged) ? status.staged : [];
61
+
55
62
  // Check for common patterns
56
63
  if (modified.some(f => String(f).includes('package.json'))) {
57
64
  insights.push('📦 Dependencies may have changed - consider updating package-lock.json');
58
65
  }
59
-
66
+
60
67
  if (created.some(f => String(f).includes('.env'))) {
61
68
  insights.push('🔐 New environment file detected - ensure it\'s in .gitignore');
62
69
  }
63
-
70
+
64
71
  if (modified.some(f => String(f).includes('README'))) {
65
72
  insights.push('📚 Documentation updated - good practice!');
66
73
  }
67
-
74
+
68
75
  if (deleted.length > created.length + modified.length) {
69
76
  insights.push('🧹 Cleanup operation detected - removing more than adding');
70
77
  }
71
-
78
+
72
79
  if (files.length > 20) {
73
- insights.push('📊 Large changeset - consider breaking into smaller commits');
80
+ insights.push('📊 Large changeset - consider using `g split` to break into smaller commits');
74
81
  }
75
-
82
+
76
83
  // Check for test files
77
84
  const testFiles = files.filter(f => {
78
85
  const fileName = String(f);
@@ -81,7 +88,7 @@ class GitAnalyzer {
81
88
  if (testFiles.length > 0) {
82
89
  insights.push('🧪 Test files modified - great for code quality!');
83
90
  }
84
-
91
+
85
92
  // Check for config files
86
93
  const configFiles = files.filter(f => {
87
94
  const fileName = String(f);
@@ -90,7 +97,12 @@ class GitAnalyzer {
90
97
  if (configFiles.length > 0) {
91
98
  insights.push('⚙️ Configuration changes detected');
92
99
  }
93
-
100
+
101
+ // Suggest staging if nothing staged
102
+ if (staged.length === 0 && files.length > 0) {
103
+ insights.push('💡 Nothing staged yet - use `g o --all` to stage and commit everything');
104
+ }
105
+
94
106
  return insights;
95
107
  }
96
108
 
@@ -98,7 +110,11 @@ class GitAnalyzer {
98
110
  try {
99
111
  const log = await this.git.log({ maxCount: limit });
100
112
  const commits = log.all;
101
-
113
+
114
+ if (commits.length === 0) {
115
+ return { totalCommits: 0 };
116
+ }
117
+
102
118
  const analysis = {
103
119
  totalCommits: commits.length,
104
120
  authors: [...new Set(commits.map(c => c.author_name))],
@@ -106,7 +122,7 @@ class GitAnalyzer {
106
122
  conventionalCommits: commits.filter(c => /^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?:/.test(c.message)).length,
107
123
  recentActivity: this.analyzeRecentActivity(commits)
108
124
  };
109
-
125
+
110
126
  return analysis;
111
127
  } catch (error) {
112
128
  return { error: error.message };
@@ -117,10 +133,10 @@ class GitAnalyzer {
117
133
  const now = new Date();
118
134
  const oneDayAgo = new Date(now - 24 * 60 * 60 * 1000);
119
135
  const oneWeekAgo = new Date(now - 7 * 24 * 60 * 60 * 1000);
120
-
136
+
121
137
  const recentCommits = commits.filter(c => new Date(c.date) > oneDayAgo);
122
138
  const weeklyCommits = commits.filter(c => new Date(c.date) > oneWeekAgo);
123
-
139
+
124
140
  return {
125
141
  last24h: recentCommits.length,
126
142
  lastWeek: weeklyCommits.length,
@@ -133,16 +149,20 @@ class GitAnalyzer {
133
149
  const additions = lines.filter(l => l.startsWith('+')).length;
134
150
  const deletions = lines.filter(l => l.startsWith('-')).length;
135
151
  const files = (diff.match(/diff --git/g) || []).length;
136
-
152
+
137
153
  let complexity = 'simple';
154
+ let emoji = '🟢';
138
155
  if (files > 10 || additions + deletions > 500) {
139
156
  complexity = 'complex';
157
+ emoji = '🔴';
140
158
  } else if (files > 5 || additions + deletions > 100) {
141
159
  complexity = 'moderate';
160
+ emoji = '🟡';
142
161
  }
143
-
162
+
144
163
  return {
145
164
  complexity,
165
+ emoji,
146
166
  files,
147
167
  additions,
148
168
  deletions,
@@ -151,81 +171,132 @@ class GitAnalyzer {
151
171
  }
152
172
 
153
173
  formatStatusOutput(enhancedStatus) {
154
- const {
155
- files = [],
156
- staged = [],
157
- modified = [],
158
- created = [],
159
- deleted = [],
160
- not_added = [],
161
- insights = [],
162
- summary = 'Unknown status'
174
+ const {
175
+ files = [],
176
+ staged = [],
177
+ modified = [],
178
+ created = [],
179
+ deleted = [],
180
+ not_added = [],
181
+ insights = [],
182
+ summary = 'Unknown status',
183
+ sessionStats = {},
184
+ branchContext = {},
163
185
  } = enhancedStatus;
164
-
186
+
165
187
  let output = '';
166
-
167
- // Header
168
- output += `${color.bold('Git Status')}\n`;
169
- output += `${color.dim(summary)}\n\n`;
170
-
188
+
189
+ // Header with branch info
190
+ output += `${color.bold('Git Status')}`;
191
+ if (branchContext.branch) {
192
+ output += ` ${color.dim('on')} ${color.cyan(branchContext.branch)}`;
193
+ if (branchContext.type) {
194
+ output += ` ${color.dim(`(${branchContext.type})`)}`;
195
+ }
196
+ }
197
+ output += '\n';
198
+ output += `${color.dim(summary)}\n`;
199
+
200
+ // Session stats
201
+ if (sessionStats.timeSinceLastCommit) {
202
+ output += `${color.dim(`Last commit: ${sessionStats.timeSinceLastCommit}`)}`;
203
+ if (sessionStats.commitsToday > 0) {
204
+ output += `${color.dim(` • ${sessionStats.commitsToday} commit${sessionStats.commitsToday > 1 ? 's' : ''} today`)}`;
205
+ }
206
+ output += '\n';
207
+ }
208
+ output += '\n';
209
+
171
210
  // Staged changes
172
211
  if (staged.length > 0) {
173
212
  output += `${color.green('Staged for commit:')}\n`;
174
213
  staged.forEach(file => {
175
- output += ` ${color.green('+')} ${file}\n`;
214
+ const emoji = Intelligence.getFileEmoji(file);
215
+ output += ` ${color.green('+')} ${emoji} ${file}\n`;
176
216
  });
177
217
  output += '\n';
178
218
  }
179
-
219
+
180
220
  // Modified files
181
221
  if (modified.length > 0) {
182
222
  output += `${color.yellow('Modified (not staged):')}\n`;
183
223
  modified.forEach(file => {
184
- output += ` ${color.yellow('M')} ${file}\n`;
224
+ const emoji = Intelligence.getFileEmoji(file);
225
+ output += ` ${color.yellow('M')} ${emoji} ${file}\n`;
185
226
  });
186
227
  output += '\n';
187
228
  }
188
-
229
+
189
230
  // New files
190
231
  if (created.length > 0) {
191
232
  output += `${color.cyan('New files:')}\n`;
192
233
  created.forEach(file => {
193
- output += ` ${color.cyan('N')} ${file}\n`;
234
+ const emoji = Intelligence.getFileEmoji(file);
235
+ output += ` ${color.cyan('N')} ${emoji} ${file}\n`;
194
236
  });
195
237
  output += '\n';
196
238
  }
197
-
239
+
198
240
  // Deleted files
199
241
  if (deleted.length > 0) {
200
242
  output += `${color.red('Deleted:')}\n`;
201
243
  deleted.forEach(file => {
202
- output += ` ${color.red('D')} ${file}\n`;
244
+ const emoji = Intelligence.getFileEmoji(file);
245
+ output += ` ${color.red('D')} ${emoji} ${file}\n`;
203
246
  });
204
247
  output += '\n';
205
248
  }
206
-
249
+
207
250
  // Untracked files
208
251
  if (not_added.length > 0) {
209
252
  output += `${color.dim('Untracked files:')}\n`;
210
253
  not_added.slice(0, 10).forEach(file => {
211
- output += ` ${color.dim('?')} ${file}\n`;
254
+ const emoji = Intelligence.getFileEmoji(file);
255
+ output += ` ${color.dim('?')} ${emoji} ${file}\n`;
212
256
  });
213
257
  if (not_added.length > 10) {
214
258
  output += ` ${color.dim(`... and ${not_added.length - 10} more`)}\n`;
215
259
  }
216
260
  output += '\n';
217
261
  }
218
-
262
+
219
263
  // AI Insights
220
264
  if (insights.length > 0) {
221
- output += `${color.cyan('💡 AI Insights:')}\n`;
265
+ output += `${color.cyan('💡 Insights:')}\n`;
222
266
  insights.forEach(insight => {
223
267
  output += ` ${insight}\n`;
224
268
  });
225
269
  }
226
-
270
+
227
271
  return output;
228
272
  }
273
+
274
+ /**
275
+ * Get today's commits formatted nicely
276
+ */
277
+ async getTodayCommits() {
278
+ try {
279
+ const today = new Date();
280
+ today.setHours(0, 0, 0, 0);
281
+ const log = await this.git.log({ '--since': today.toISOString() });
282
+ return log.all;
283
+ } catch (error) {
284
+ return [];
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Format commit for display
290
+ */
291
+ formatCommit(commit, index) {
292
+ const hash = color.yellow(commit.hash.substring(0, 7));
293
+ const message = commit.message.split('\n')[0];
294
+ const time = new Date(commit.date).toLocaleTimeString('en-US', {
295
+ hour: '2-digit',
296
+ minute: '2-digit'
297
+ });
298
+ return ` ${color.dim(`${index + 1}.`)} ${hash} ${message} ${color.dim(`(${time})`)}`;
299
+ }
229
300
  }
230
301
 
231
302
  module.exports = { GitAnalyzer };
@@ -8,8 +8,23 @@ const color = {
8
8
  cyan: (s) => `\x1b[36m${s}\x1b[0m`,
9
9
  blue: (s) => `\x1b[34m${s}\x1b[0m`,
10
10
  magenta: (s) => `\x1b[35m${s}\x1b[0m`,
11
+ white: (s) => `\x1b[37m${s}\x1b[0m`,
12
+ gray: (s) => `\x1b[90m${s}\x1b[0m`,
11
13
  bold: (s) => `\x1b[1m${s}\x1b[0m`,
12
14
  dim: (s) => `\x1b[2m${s}\x1b[0m`,
15
+ italic: (s) => `\x1b[3m${s}\x1b[0m`,
16
+ underline: (s) => `\x1b[4m${s}\x1b[0m`,
17
+ // Background colors
18
+ bgGreen: (s) => `\x1b[42m\x1b[30m${s}\x1b[0m`,
19
+ bgYellow: (s) => `\x1b[43m\x1b[30m${s}\x1b[0m`,
20
+ bgRed: (s) => `\x1b[41m\x1b[37m${s}\x1b[0m`,
21
+ bgCyan: (s) => `\x1b[46m\x1b[30m${s}\x1b[0m`,
22
+ bgBlue: (s) => `\x1b[44m\x1b[37m${s}\x1b[0m`,
23
+ // Compound styles
24
+ success: (s) => `\x1b[32m✓\x1b[0m ${s}`,
25
+ warning: (s) => `\x1b[33m⚠\x1b[0m ${s}`,
26
+ error: (s) => `\x1b[31m✗\x1b[0m ${s}`,
27
+ info: (s) => `\x1b[36mℹ\x1b[0m ${s}`,
13
28
  reset: '\x1b[0m'
14
29
  };
15
30