commitect 1.0.0

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.
Files changed (55) hide show
  1. package/.github/workflows/publish.yml +39 -0
  2. package/LICENSE +21 -0
  3. package/README.md +176 -0
  4. package/dist/commands/analyze.d.ts +2 -0
  5. package/dist/commands/analyze.d.ts.map +1 -0
  6. package/dist/commands/analyze.js +40 -0
  7. package/dist/commands/analyze.js.map +1 -0
  8. package/dist/commands/clear-cache.d.ts +2 -0
  9. package/dist/commands/clear-cache.d.ts.map +1 -0
  10. package/dist/commands/clear-cache.js +23 -0
  11. package/dist/commands/clear-cache.js.map +1 -0
  12. package/dist/commands/commit.d.ts +2 -0
  13. package/dist/commands/commit.d.ts.map +1 -0
  14. package/dist/commands/commit.js +42 -0
  15. package/dist/commands/commit.js.map +1 -0
  16. package/dist/commands/copy.d.ts +2 -0
  17. package/dist/commands/copy.d.ts.map +1 -0
  18. package/dist/commands/copy.js +42 -0
  19. package/dist/commands/copy.js.map +1 -0
  20. package/dist/commands/help.d.ts +2 -0
  21. package/dist/commands/help.d.ts.map +1 -0
  22. package/dist/commands/help.js +129 -0
  23. package/dist/commands/help.js.map +1 -0
  24. package/dist/commands/history.d.ts +2 -0
  25. package/dist/commands/history.d.ts.map +1 -0
  26. package/dist/commands/history.js +58 -0
  27. package/dist/commands/history.js.map +1 -0
  28. package/dist/index.d.ts +3 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +44 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/services/llm.d.ts +6 -0
  33. package/dist/services/llm.d.ts.map +1 -0
  34. package/dist/services/llm.js +94 -0
  35. package/dist/services/llm.js.map +1 -0
  36. package/dist/utils/cache.d.ts +61 -0
  37. package/dist/utils/cache.d.ts.map +1 -0
  38. package/dist/utils/cache.js +141 -0
  39. package/dist/utils/cache.js.map +1 -0
  40. package/dist/utils/git.d.ts +5 -0
  41. package/dist/utils/git.d.ts.map +1 -0
  42. package/dist/utils/git.js +69 -0
  43. package/dist/utils/git.js.map +1 -0
  44. package/package.json +38 -0
  45. package/src/commands/analyze.ts +44 -0
  46. package/src/commands/clear-cache.ts +23 -0
  47. package/src/commands/commit.ts +48 -0
  48. package/src/commands/copy.ts +48 -0
  49. package/src/commands/help.ts +143 -0
  50. package/src/commands/history.ts +62 -0
  51. package/src/index.ts +53 -0
  52. package/src/services/llm.ts +123 -0
  53. package/src/utils/cache.ts +170 -0
  54. package/src/utils/git.ts +74 -0
  55. package/tsconfig.json +20 -0
@@ -0,0 +1,48 @@
1
+ import { isGitRepository, getGitDiff, hasChanges, executeCommit } from '../utils/git.js';
2
+ import { generateCommitMessage } from '../services/llm.js';
3
+ import chalk from 'chalk';
4
+
5
+ export async function commitCommand(): Promise<void> {
6
+ try {
7
+ // Validate git repository
8
+ if (!isGitRepository()) {
9
+ console.error(chalk.red('❌ Not a git repository'));
10
+ process.exit(1);
11
+ }
12
+
13
+ // Check for changes
14
+ if (!hasChanges()) {
15
+ console.log(chalk.yellow('⚠ No changes to commit'));
16
+ process.exit(0);
17
+ }
18
+
19
+ // Get diff
20
+ const diff = getGitDiff();
21
+
22
+ if (!diff.trim()) {
23
+ console.log(chalk.yellow('⚠ No changes to commit'));
24
+ process.exit(0);
25
+ }
26
+
27
+ // Generate commit message
28
+ console.log(chalk.blue('🔍 Generating commit message...'));
29
+ const suggestion = await generateCommitMessage(diff);
30
+
31
+ // Build commit message as "intent: message"
32
+ const commitMessage = `${suggestion.intent}: ${suggestion.message}`;
33
+
34
+ // Execute git commit
35
+ console.log(chalk.blue('💾 Committing changes...'));
36
+ executeCommit(commitMessage);
37
+
38
+ console.log(chalk.green('✔ Committed: ') + commitMessage);
39
+
40
+ } catch (error) {
41
+ if (error instanceof Error) {
42
+ console.error(chalk.red('❌ ' + error.message));
43
+ } else {
44
+ console.error(chalk.red('❌ An unexpected error occurred'));
45
+ }
46
+ process.exit(1);
47
+ }
48
+ }
@@ -0,0 +1,48 @@
1
+ import { isGitRepository, getGitDiff, hasChanges } from '../utils/git.js';
2
+ import { generateCommitMessage } from '../services/llm.js';
3
+ import clipboardy from 'clipboardy';
4
+ import chalk from 'chalk';
5
+
6
+ export async function copyCommand(): Promise<void> {
7
+ try {
8
+ // Validate git repository
9
+ if (!isGitRepository()) {
10
+ console.error(chalk.red('❌ Not a git repository'));
11
+ process.exit(1);
12
+ }
13
+
14
+ // Check for changes
15
+ if (!hasChanges()) {
16
+ console.log(chalk.yellow('⚠ No changes detected'));
17
+ process.exit(0);
18
+ }
19
+
20
+ // Get diff
21
+ const diff = getGitDiff();
22
+
23
+ if (!diff.trim()) {
24
+ console.log(chalk.yellow('⚠ No changes to analyze'));
25
+ process.exit(0);
26
+ }
27
+
28
+ // Generate commit message
29
+ console.log(chalk.blue('🔍 Generating commit message...'));
30
+ const suggestion = await generateCommitMessage(diff);
31
+
32
+ // Build commit message as "intent: message"
33
+ const commitMessage = `${suggestion.intent}: ${suggestion.message}`;
34
+
35
+ // Copy to clipboard
36
+ await clipboardy.write(commitMessage);
37
+
38
+ console.log(chalk.green('✔ Commit message copied to clipboard'));
39
+
40
+ } catch (error) {
41
+ if (error instanceof Error) {
42
+ console.error(chalk.red('❌ ' + error.message));
43
+ } else {
44
+ console.error(chalk.red('❌ An unexpected error occurred'));
45
+ }
46
+ process.exit(1);
47
+ }
48
+ }
@@ -0,0 +1,143 @@
1
+ import chalk from 'chalk';
2
+
3
+ const VERSION = '1.0.0';
4
+ const GITHUB_REPO = 'https://github.com/Mohammed_3tef/CommiTect_CLI';
5
+ const ISSUES_URL = GITHUB_REPO + '/issues';
6
+
7
+ export function helpCommand(): void {
8
+ console.log('');
9
+ console.log(chalk.bold.cyan('╔══════════════════════════════════════════════════════════╗'));
10
+ console.log(chalk.bold.cyan('║ ') + chalk.bold.white('CommiTect') + chalk.bold.cyan(' ║'));
11
+ console.log(chalk.bold.cyan('║ ') + chalk.gray('Zero-config Git Commit Assistant') + chalk.bold.cyan(' ║'));
12
+ console.log(chalk.bold.cyan('╚══════════════════════════════════════════════════════════╝'));
13
+ console.log('');
14
+
15
+ // COMMANDS SECTION
16
+ console.log(chalk.bold.yellow('📋 AVAILABLE COMMANDS'));
17
+ console.log('');
18
+
19
+ // ANALYZE
20
+ console.log(chalk.bold.green(' commitect analyze'));
21
+ console.log(chalk.gray(' │'));
22
+ console.log(chalk.gray(' ├─ ') + 'Analyzes your git changes and suggests a commit message');
23
+ console.log(chalk.gray(' ├─ ') + 'Displays both intent and message on the terminal');
24
+ console.log(chalk.gray(' └─ ') + 'Does NOT modify your git repository');
25
+ console.log('');
26
+ console.log(chalk.dim(' Example output:'));
27
+ console.log(chalk.dim(' Feature: Add user authentication with JWT'));
28
+ console.log('');
29
+
30
+ // COPY
31
+ console.log(chalk.bold.green(' commitect copy'));
32
+ console.log(chalk.gray(' │'));
33
+ console.log(chalk.gray(' ├─ ') + 'Generates a commit message from your changes');
34
+ console.log(chalk.gray(' ├─ ') + 'Copies ONLY the message (not intent) to clipboard');
35
+ console.log(chalk.gray(' └─ ') + 'Perfect for manual commits with custom flags');
36
+ console.log('');
37
+ console.log(chalk.dim(' Usage:'));
38
+ console.log(chalk.dim(' $ commitect copy'));
39
+ console.log(chalk.dim(' $ git commit -m "<paste>" --no-verify'));
40
+ console.log('');
41
+
42
+ // COMMIT
43
+ console.log(chalk.bold.green(' commitect commit'));
44
+ console.log(chalk.gray(' │'));
45
+ console.log(chalk.gray(' ├─ ') + 'Generates a commit message from your changes');
46
+ console.log(chalk.gray(' ├─ ') + 'Automatically executes: git commit -m "<message>"');
47
+ console.log(chalk.gray(' └─ ') + 'Fastest way to commit with AI-generated messages');
48
+ console.log('');
49
+ console.log(chalk.dim(' Warning: Make sure you have staged your changes first!'));
50
+ console.log(chalk.dim(' $ git add .'));
51
+ console.log(chalk.dim(' $ commitect commit'));
52
+ console.log('');
53
+
54
+ // HISTORY
55
+ console.log(chalk.bold.green(' commitect history'));
56
+ console.log(chalk.gray(' │'));
57
+ console.log(chalk.gray(' ├─ ') + 'Shows all cached commit messages');
58
+ console.log(chalk.gray(' ├─ ') + 'Displays timestamp and time ago for each entry');
59
+ console.log(chalk.gray(' └─ ') + 'Useful for reviewing past suggestions');
60
+ console.log('');
61
+
62
+ // CLEAR-CACHE
63
+ console.log(chalk.bold.green(' commitect clear-cache'));
64
+ console.log(chalk.gray(' │'));
65
+ console.log(chalk.gray(' ├─ ') + 'Clears all cached commit messages');
66
+ console.log(chalk.gray(' ├─ ') + 'Cache location: ~/.commitect/cache.json');
67
+ console.log(chalk.gray(' └─ ') + 'Use when you want fresh suggestions');
68
+ console.log('');
69
+
70
+ // HELP
71
+ console.log(chalk.bold.green(' commitect help'));
72
+ console.log(chalk.gray(' │'));
73
+ console.log(chalk.gray(' └─ ') + 'Shows this help message');
74
+ console.log('');
75
+
76
+ // HOW IT WORKS
77
+ console.log(chalk.bold.yellow('⚙️ HOW IT WORKS'));
78
+ console.log('');
79
+ console.log(chalk.gray(' 1. ') + '📖 Reads your git diff (staged + unstaged changes)');
80
+ console.log(chalk.gray(' 2. ') + '🔍 Checks cache for previously analyzed diffs');
81
+ console.log(chalk.gray(' 3. ') + '🤖 Sends to AI API if not cached (with auto-retry)');
82
+ console.log(chalk.gray(' 4. ') + '💾 Caches result for 30 days');
83
+ console.log(chalk.gray(' 5. ') + '✨ Returns professional commit message');
84
+ console.log('');
85
+
86
+ // WORKFLOW
87
+ console.log(chalk.bold.yellow('🔄 TYPICAL WORKFLOW'));
88
+ console.log('');
89
+ console.log(chalk.gray(' # Make your changes'));
90
+ console.log(chalk.white(' $ vim src/auth.ts'));
91
+ console.log('');
92
+ console.log(chalk.gray(' # Stage files'));
93
+ console.log(chalk.white(' $ git add .'));
94
+ console.log('');
95
+ console.log(chalk.gray(' # Option 1: Preview message'));
96
+ console.log(chalk.white(' $ commitect analyze'));
97
+ console.log('');
98
+ console.log(chalk.gray(' # Option 2: Copy to clipboard'));
99
+ console.log(chalk.white(' $ commitect copy'));
100
+ console.log(chalk.white(' $ git commit -m "<paste>"'));
101
+ console.log('');
102
+ console.log(chalk.gray(' # Option 3: Auto-commit (fastest)'));
103
+ console.log(chalk.white(' $ commitect commit'));
104
+ console.log('');
105
+
106
+ // FEATURES
107
+ console.log(chalk.bold.yellow('✨ KEY FEATURES'));
108
+ console.log('');
109
+ console.log(chalk.green(' ✓ ') + 'Zero configuration required');
110
+ console.log(chalk.green(' ✓ ') + 'Smart caching (instant responses for same diffs)');
111
+ console.log(chalk.green(' ✓ ') + 'Auto-retry on API failures (up to 3 attempts)');
112
+ console.log(chalk.green(' ✓ ') + 'Ignores: node_modules/, dist/, build/, .git/');
113
+ console.log(chalk.green(' ✓ ') + 'Professional messages (imperative, <70 chars)');
114
+ console.log(chalk.green(' ✓ ') + 'Works with any git repository');
115
+ console.log('');
116
+
117
+ // TIPS
118
+ console.log(chalk.bold.yellow('💡 PRO TIPS'));
119
+ console.log('');
120
+ console.log(chalk.cyan(' • ') + 'Use ' + chalk.bold('analyze') + ' when you want to review before committing');
121
+ console.log(chalk.cyan(' • ') + 'Use ' + chalk.bold('copy') + ' when you need custom git flags');
122
+ console.log(chalk.cyan(' • ') + 'Use ' + chalk.bold('commit') + ' for quick, everyday commits');
123
+ console.log(chalk.cyan(' • ') + 'Use ' + chalk.bold('history') + ' to review all your cached messages');
124
+ console.log(chalk.cyan(' • ') + 'Run ' + chalk.bold('clear-cache') + ' if suggestions seem outdated');
125
+ console.log(chalk.cyan(' • ') + 'Cache saves time and reduces API costs significantly');
126
+ console.log('');
127
+
128
+ // REQUIREMENTS
129
+ console.log(chalk.bold.yellow('📦 REQUIREMENTS'));
130
+ console.log('');
131
+ console.log(chalk.gray(' • Node.js >= 16'));
132
+ console.log(chalk.gray(' • Git repository (initialized)'));
133
+ console.log(chalk.gray(' • Internet connection (unless result is cached)'));
134
+ console.log('');
135
+
136
+ // FOOTER
137
+ console.log(chalk.bold.cyan('─'.repeat(63)));
138
+ console.log(chalk.gray(' Version: ') + chalk.white(VERSION));
139
+ console.log(chalk.gray(' Docs: ') + chalk.white(GITHUB_REPO));
140
+ console.log(chalk.gray(' Issues: ') + chalk.white(ISSUES_URL));
141
+ console.log(chalk.bold.cyan('─'.repeat(63)));
142
+ console.log('');
143
+ }
@@ -0,0 +1,62 @@
1
+ import { commitCache } from '../utils/cache.js';
2
+ import chalk from 'chalk';
3
+
4
+ export function historyCommand(): void {
5
+ try {
6
+ const history = commitCache.getHistory();
7
+
8
+ if (history.length === 0) {
9
+ console.log(chalk.yellow('ℹ No commit history found'));
10
+ console.log(chalk.gray(' Generate some commits first using commitect analyze/copy/commit'));
11
+ return;
12
+ }
13
+
14
+ console.log('');
15
+ console.log(chalk.bold.cyan('📜 COMMIT HISTORY'));
16
+ console.log(chalk.gray('─'.repeat(70)));
17
+ console.log('');
18
+
19
+ history.forEach((entry, index) => {
20
+ const date = new Date(entry.timestamp);
21
+ const timeAgo = getTimeAgo(entry.timestamp);
22
+
23
+ // Format: [1] Feature: Add user authentication
24
+ console.log(chalk.bold.white(`[${index + 1}]`) + ' ' + chalk.green(`${entry.intent}: ${entry.message}`));
25
+ console.log(chalk.gray(` 📁 ${entry.folder}`));
26
+ console.log(chalk.gray(` 🕒 ${date.toLocaleString()} (${timeAgo})`));
27
+ console.log('');
28
+ });
29
+
30
+ console.log(chalk.gray('─'.repeat(70)));
31
+ console.log(chalk.gray(`Total: ${history.length} cached commit message${history.length !== 1 ? 's' : ''}`));
32
+ console.log('');
33
+
34
+ } catch (error) {
35
+ if (error instanceof Error) {
36
+ console.error(chalk.red('❌ ' + error.message));
37
+ } else {
38
+ console.error(chalk.red('❌ Failed to load history'));
39
+ }
40
+ process.exit(1);
41
+ }
42
+ }
43
+
44
+ function getTimeAgo(timestamp: number): string {
45
+ const now = Date.now();
46
+ const diff = now - timestamp;
47
+
48
+ const seconds = Math.floor(diff / 1000);
49
+ const minutes = Math.floor(seconds / 60);
50
+ const hours = Math.floor(minutes / 60);
51
+ const days = Math.floor(hours / 24);
52
+
53
+ if (days > 0) {
54
+ return `${days} day${days !== 1 ? 's' : ''} ago`;
55
+ } else if (hours > 0) {
56
+ return `${hours} hour${hours !== 1 ? 's' : ''} ago`;
57
+ } else if (minutes > 0) {
58
+ return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`;
59
+ } else {
60
+ return 'just now';
61
+ }
62
+ }
package/src/index.ts ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { analyzeCommand } from './commands/analyze.js';
4
+ import { copyCommand } from './commands/copy.js';
5
+ import { commitCommand } from './commands/commit.js';
6
+ import { clearCacheCommand } from './commands/clear-cache.js';
7
+ import { historyCommand } from './commands/history.js';
8
+ import { helpCommand } from './commands/help.js';
9
+
10
+ const program = new Command();
11
+
12
+ program
13
+ .name('commitect')
14
+ .description('Zero-config Git Commit Assistant')
15
+ .version('1.0.0');
16
+
17
+ program
18
+ .command('analyze')
19
+ .description('Analyze changes and suggest a commit message')
20
+ .action(analyzeCommand);
21
+
22
+ program
23
+ .command('copy')
24
+ .description('Generate commit message and copy to clipboard')
25
+ .action(copyCommand);
26
+
27
+ program
28
+ .command('commit')
29
+ .description('Generate and execute git commit')
30
+ .action(commitCommand);
31
+
32
+ program
33
+ .command('history')
34
+ .description('Show cached commit message history')
35
+ .action(historyCommand);
36
+
37
+ program
38
+ .command('clear-cache')
39
+ .description('Clear the commit message cache')
40
+ .action(clearCacheCommand);
41
+
42
+ program
43
+ .command('help')
44
+ .description('Show detailed help and examples')
45
+ .action(helpCommand);
46
+
47
+ // Show help by default if no command is provided
48
+ if (process.argv.length === 2) {
49
+ helpCommand();
50
+ process.exit(0);
51
+ }
52
+
53
+ program.parse();
@@ -0,0 +1,123 @@
1
+ import axios, { AxiosError } from 'axios';
2
+ import { commitCache } from '../utils/cache.js';
3
+
4
+ export interface CommitSuggestion {
5
+ intent: string;
6
+ message: string;
7
+ }
8
+
9
+ const API_ENDPOINT = 'http://commitintentdetector.runasp.net/api/Commit/analyze';
10
+
11
+ export async function generateCommitMessage(diff: string): Promise<CommitSuggestion> {
12
+ // Check cache first
13
+ const cached = commitCache.get(diff);
14
+ if (cached) {
15
+ return cached;
16
+ }
17
+ const maxRetries = 3;
18
+ let lastError: Error | null = null;
19
+
20
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
21
+ try {
22
+ const response = await axios.post(
23
+ API_ENDPOINT,
24
+ { diff },
25
+ {
26
+ headers: {
27
+ 'Content-Type': 'application/json'
28
+ },
29
+ timeout: 30000 // 30 second timeout
30
+ }
31
+ );
32
+
33
+ // API returns: { intent: "Intent: Feature\nMessage: Add subtraction support function" }
34
+ const data = response.data;
35
+
36
+ if (!data || !data.intent) {
37
+ throw new Error('Invalid response format from API');
38
+ }
39
+
40
+ const result = parseResponse(data.intent);
41
+
42
+ // Cache the result
43
+ commitCache.set(diff, result.intent, result.message);
44
+
45
+ return result;
46
+
47
+ } catch (error) {
48
+ lastError = error as Error;
49
+
50
+ if (axios.isAxiosError(error)) {
51
+ const axiosError = error as AxiosError;
52
+
53
+ // Handle rate limiting (429)
54
+ if (axiosError.response?.status === 429) {
55
+ if (attempt < maxRetries) {
56
+ await sleep(1000 * attempt); // Exponential backoff
57
+ continue;
58
+ }
59
+ throw new Error('API rate limit reached. Please try again later.');
60
+ }
61
+
62
+ // Handle server errors (5xx) - retry
63
+ if (axiosError.response?.status && axiosError.response.status >= 500) {
64
+ if (attempt < maxRetries) {
65
+ await sleep(1000 * attempt);
66
+ continue;
67
+ }
68
+ }
69
+
70
+ // Handle network errors - retry
71
+ if (axiosError.code === 'ECONNREFUSED' || axiosError.code === 'ETIMEDOUT') {
72
+ if (attempt < maxRetries) {
73
+ await sleep(1000 * attempt);
74
+ continue;
75
+ }
76
+ throw new Error('Unable to connect to API. Please check your network connection.');
77
+ }
78
+ }
79
+
80
+ // Don't retry on client errors (4xx) except 429
81
+ if (axios.isAxiosError(error) && error.response?.status && error.response.status < 500 && error.response.status !== 429) {
82
+ break;
83
+ }
84
+
85
+ // Retry on other errors
86
+ if (attempt < maxRetries) {
87
+ await sleep(1000 * attempt);
88
+ continue;
89
+ }
90
+ }
91
+ }
92
+
93
+ throw new Error('Failed to generate commit message. Please check your API configuration.');
94
+ }
95
+
96
+ function parseResponse(response: string): CommitSuggestion {
97
+ const lines = response.trim().split('\n');
98
+
99
+ let intent = '';
100
+ let message = '';
101
+
102
+ for (const line of lines) {
103
+ if (line.startsWith('Intent:')) {
104
+ intent = line.replace('Intent:', '').trim();
105
+ } else if (line.startsWith('Message:')) {
106
+ message = line.replace('Message:', '').trim();
107
+ }
108
+ }
109
+
110
+ if (!intent || !message) {
111
+ throw new Error('Invalid response format from LLM');
112
+ }
113
+
114
+ if (message.length > 70) {
115
+ message = message.substring(0, 67) + '...';
116
+ }
117
+
118
+ return { intent, message };
119
+ }
120
+
121
+ function sleep(ms: number): Promise<void> {
122
+ return new Promise(resolve => setTimeout(resolve, ms));
123
+ }
@@ -0,0 +1,170 @@
1
+ import { createHash } from 'crypto';
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
3
+ import { homedir } from 'os';
4
+ import { join, basename } from 'path';
5
+
6
+ interface CacheEntry {
7
+ hash: string;
8
+ intent: string;
9
+ message: string;
10
+ timestamp: number;
11
+ folder: string;
12
+ }
13
+
14
+ const CACHE_DIR = join(homedir(), '.commitect');
15
+ const CACHE_FILE = join(CACHE_DIR, 'cache.json');
16
+ const CACHE_MAX_AGE = 30 * 24 * 60 * 60 * 1000; // 30 days in milliseconds
17
+
18
+ class CommitCache {
19
+ private cache: Map<string, CacheEntry>;
20
+
21
+ constructor() {
22
+ this.cache = new Map();
23
+ this.loadCache();
24
+ }
25
+
26
+ /**
27
+ * Generate a hash from the git diff
28
+ */
29
+ private hashDiff(diff: string): string {
30
+ return createHash('sha256').update(diff.trim()).digest('hex');
31
+ }
32
+
33
+ /**
34
+ * Get current folder name
35
+ */
36
+ private getCurrentFolder(): string {
37
+ return basename(process.cwd());
38
+ }
39
+
40
+ /**
41
+ * Load cache from disk
42
+ */
43
+ private loadCache(): void {
44
+ try {
45
+ if (!existsSync(CACHE_DIR)) {
46
+ mkdirSync(CACHE_DIR, { recursive: true });
47
+ }
48
+
49
+ if (existsSync(CACHE_FILE)) {
50
+ const data = readFileSync(CACHE_FILE, 'utf-8');
51
+ const entries: CacheEntry[] = JSON.parse(data);
52
+
53
+ // Load valid entries (not expired)
54
+ const now = Date.now();
55
+ entries.forEach(entry => {
56
+ if (now - entry.timestamp < CACHE_MAX_AGE) {
57
+ this.cache.set(entry.hash, entry);
58
+ }
59
+ });
60
+ }
61
+ } catch (error) {
62
+ // If cache is corrupted, start fresh
63
+ this.cache.clear();
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Save cache to disk
69
+ */
70
+ private saveCache(): void {
71
+ try {
72
+ if (!existsSync(CACHE_DIR)) {
73
+ mkdirSync(CACHE_DIR, { recursive: true });
74
+ }
75
+
76
+ const entries = Array.from(this.cache.values());
77
+ writeFileSync(CACHE_FILE, JSON.stringify(entries, null, 2), 'utf-8');
78
+ } catch (error) {
79
+ // Silently fail - caching is optional
80
+ console.warn('Warning: Failed to save cache');
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Get cached commit message for a diff
86
+ */
87
+ get(diff: string): { intent: string; message: string; folder: string } | null {
88
+ const hash = this.hashDiff(diff);
89
+ const entry = this.cache.get(hash);
90
+
91
+ if (!entry) {
92
+ return null;
93
+ }
94
+
95
+ // Check if entry is still valid
96
+ const now = Date.now();
97
+ if (now - entry.timestamp > CACHE_MAX_AGE) {
98
+ this.cache.delete(hash);
99
+ this.saveCache();
100
+ return null;
101
+ }
102
+
103
+ return {
104
+ intent: entry.intent,
105
+ message: entry.message,
106
+ folder: entry.folder
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Store a commit message in cache
112
+ */
113
+ set(diff: string, intent: string, message: string): void {
114
+ const hash = this.hashDiff(diff);
115
+
116
+ this.cache.set(hash, {
117
+ hash,
118
+ intent,
119
+ message,
120
+ timestamp: Date.now(),
121
+ folder: this.getCurrentFolder()
122
+ });
123
+
124
+ this.saveCache();
125
+ }
126
+
127
+ /**
128
+ * Clear all cache
129
+ */
130
+ clear(): void {
131
+ this.cache.clear();
132
+ this.saveCache();
133
+ }
134
+
135
+ /**
136
+ * Get cache statistics
137
+ */
138
+ getStats(): { size: number; oldestEntry: number | null } {
139
+ const entries = Array.from(this.cache.values());
140
+ const oldestEntry = entries.length > 0
141
+ ? Math.min(...entries.map(e => e.timestamp))
142
+ : null;
143
+
144
+ return {
145
+ size: this.cache.size,
146
+ oldestEntry
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Get all cache entries sorted by timestamp (newest first)
152
+ */
153
+ getHistory(): CacheEntry[] {
154
+ const entries = Array.from(this.cache.values());
155
+ return entries.sort((a, b) => b.timestamp - a.timestamp);
156
+ }
157
+
158
+ /**
159
+ * Get cache entries for a specific folder
160
+ */
161
+ getHistoryByFolder(folder: string): CacheEntry[] {
162
+ const entries = Array.from(this.cache.values());
163
+ return entries
164
+ .filter(entry => entry.folder === folder)
165
+ .sort((a, b) => b.timestamp - a.timestamp);
166
+ }
167
+ }
168
+
169
+ // Singleton instance
170
+ export const commitCache = new CommitCache();