Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Models/AppConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ public class AppConfiguration
[JsonPropertyName("default_model")]
public string? DefaultModel { get; set; }

[JsonPropertyName("openai_endpoint")]
public string? OpenAiEndpoint { get; set; }

[JsonPropertyName("default_temperature")]
public int? DefaultTemperature { get; set; }

Expand Down
29 changes: 11 additions & 18 deletions Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@ static async Task<int> Main(string[] args)
"Generate commit message without committing"
);
var verboseOption = new Option<bool>("--verbose", "Show detailed output");
var patternOption = new Option<string>(
"--pattern",
() => PatternNames.CommitPattern,
"Pattern to use for message generation"
);
var temperatureOption = new Option<int>(
"--temperature",
() => 1,
Expand All @@ -31,10 +26,9 @@ static async Task<int> Main(string[] args)
() => 0,
"Frequency penalty for AI model"
);
var modelOption = new Option<string>(
var modelOption = new Option<string?>(
"--model",
() => "gpt-4o-mini",
"AI model to use (default: gpt-4o-mini)"
description: "AI model to use (overrides setup)"
);

var setupOption = new Option<bool>(
Expand All @@ -46,7 +40,6 @@ static async Task<int> Main(string[] args)
{
dryRunOption,
verboseOption,
patternOption,
temperatureOption,
topPOption,
presenceOption,
Expand All @@ -59,12 +52,11 @@ static async Task<int> Main(string[] args)
async (
bool dryRun,
bool verbose,
string pattern,
int temperature,
int topP,
int presence,
int frequency,
string model
string? model
) =>
{
try
Expand All @@ -84,7 +76,6 @@ string model
await GenerateCommitMessage(
dryRun,
verbose,
pattern,
temperature,
topP,
presence,
Expand All @@ -100,7 +91,6 @@ await GenerateCommitMessage(
},
dryRunOption,
verboseOption,
patternOption,
temperatureOption,
topPOption,
presenceOption,
Expand All @@ -114,12 +104,11 @@ await GenerateCommitMessage(
static async Task GenerateCommitMessage(
bool dryRun,
bool verbose,
string pattern,
int temperature,
int topP,
int presence,
int frequency,
string model
string? model
)
{
var gitService = new GitService();
Expand All @@ -137,7 +126,10 @@ string model
}

// Create OpenAI service with the API key
var openAiService = new OpenAIService(apiKey);
var endpoint = await configService.GetOpenAiEndpointAsync() ?? "https://api.openai.com/v1";
var defaultModel = await configService.GetDefaultModelAsync() ?? "gpt-4o-mini";

var openAiService = new OpenAIService(apiKey, endpoint);

// Check if we're in a git repository
if (!Directory.Exists(".git") && !await gitService.IsInGitRepositoryAsync())
Expand Down Expand Up @@ -182,14 +174,15 @@ string model
}

// Generate commit message using OpenAI with chunking support
var finalModel = string.IsNullOrWhiteSpace(model) ? defaultModel : model;
var commitMessage = await openAiService.GenerateCommitMessageAsync(
chunks,
pattern,
PatternNames.CommitPattern,
temperature,
topP,
presence,
frequency,
model,
finalModel,
verbose
);

Expand Down
27 changes: 11 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ A cross-platform .NET tool that generates AI-powered commit messages using OpenA

- 🤖 **AI-powered commit messages** - Generate meaningful commit messages from your staged changes
- 🔄 **Cross-platform** - Works on Windows, macOS, and Linux
- 🎛️ **Highly configurable** - Adjust AI parameters and patterns to your preference
- 🎛️ **Highly configurable** - Adjust AI parameters to your preference
- 🧪 **Dry-run mode** - Preview generated messages without committing
- 📝 **Verbose output** - Detailed logging for debugging and transparency
- ⚡ **Fast and lightweight** - Direct OpenAI API integration for quick responses
Expand All @@ -17,7 +17,7 @@ A cross-platform .NET tool that generates AI-powered commit messages using OpenA
### Prerequisites

- [.NET 8.0 or later](https://dotnet.microsoft.com/download)
- OpenAI API key (either set as `OPENAI_API_KEY` environment variable or via `--setup`)
- OpenAI API key (optional, required only if your endpoint needs authentication)
- Git repository with staged changes

### Installation
Expand Down Expand Up @@ -83,13 +83,10 @@ WriteCommit --dry-run
WriteCommit --verbose

# Custom AI parameters
WriteCommit --temperature 0.7 --topp 0.9 --pattern custom_pattern

# Force reinstall all patterns
WriteCommit --reinstall-patterns
WriteCommit --temperature 0.7 --topp 0.9

# Combine multiple options
WriteCommit --dry-run --verbose --temperature 0.5 --reinstall-patterns
WriteCommit --dry-run --verbose --temperature 0.5
```

## ⚙️ Configuration Options
Expand All @@ -98,18 +95,16 @@ WriteCommit --dry-run --verbose --temperature 0.5 --reinstall-patterns
|--------|---------|-------------|
| `--dry-run` | `false` | Generate message without committing |
| `--verbose` | `false` | Show detailed output |
| `--pattern` | `write_commit_message` | Pattern to use for message generation |
| `--temperature` | `1` | AI creativity level (0-2) |
| `--topp` | `1` | Nucleus sampling parameter (0-1) |
| `--model` | `gpt-4o-mini` | OpenAI model to use |
| `--model` | from setup | OpenAI model to use |
| `--presence` | `0` | Presence penalty (-2 to 2) |
| `--frequency` | `0` | Frequency penalty (-2 to 2) |
| `--reinstall-patterns` | `false` | Force reinstallation of all patterns |
| `--setup` | `false` | Configure OpenAI API key |
| `--setup` | `false` | Configure OpenAI settings |

## 🔧 How It Works

1. **Validates environment** - Checks for git repository and OpenAI API key
1. **Validates environment** - Checks for git repository and OpenAI settings
2. **Analyzes changes** - Processes your staged git diff using semantic chunking
3. **Generates message** - Uses OpenAI API to create meaningful commit message
4. **Commits changes** - Applies the generated message (unless `--dry-run`)
Expand All @@ -125,19 +120,19 @@ WriteCommit --dry-run --verbose --temperature 0.5 --reinstall-patterns
WriteCommit --setup
```

This will prompt you to enter your API key and securely save it to `~/.writecommit/config.json`.
This will prompt you to enter your API key (if needed), API endpoint, and default model, then save them to `~/.writecommit/config.json`.

**Option 2: Using Environment Variables**

```bash
# Linux/macOS
export OPENAI_API_KEY="your-api-key-here"
export OPENAI_API_KEY="your-api-key-here" # optional

# Windows (PowerShell)
$env:OPENAI_API_KEY="your-api-key-here"
$env:OPENAI_API_KEY="your-api-key-here" # optional

# Windows (Command Prompt)
set OPENAI_API_KEY=your-api-key-here
set OPENAI_API_KEY=your-api-key-here # optional
```

For persistent configuration, add the export to your shell profile (`~/.bashrc`, `~/.zshrc`, etc.) or Windows environment variables.
Expand Down
47 changes: 35 additions & 12 deletions Services/ConfigurationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,24 @@ public async Task SaveConfigurationAsync(AppConfiguration config)
return config?.OpenAiApiKey;
}

/// <summary>
/// Gets the configured OpenAI endpoint or null if not set
/// </summary>
public async Task<string?> GetOpenAiEndpointAsync()
{
var config = await LoadConfigurationAsync();
return config?.OpenAiEndpoint;
}

/// <summary>
/// Gets the configured default model or null if not set
/// </summary>
public async Task<string?> GetDefaultModelAsync()
{
var config = await LoadConfigurationAsync();
return config?.DefaultModel;
}

/// <summary>
/// Prompts user to enter and save their OpenAI API key
/// </summary>
Expand All @@ -87,33 +105,38 @@ public async Task<bool> SetupApiKeyAsync(bool verbose = false)
Console.WriteLine("Please enter your OpenAI API key.");
Console.WriteLine("You can get one from: https://platform.openai.com/api-keys");
Console.WriteLine();
Console.Write("API Key: ");
Console.Write("API Key (leave blank if not required): ");

// Read API key with masked input
var apiKey = ReadMaskedInput();
Console.WriteLine();

if (string.IsNullOrWhiteSpace(apiKey))
{
Console.WriteLine("No API key entered. Setup cancelled.");
return false;
apiKey = null;
}

// Basic validation
if (!apiKey.StartsWith("sk-"))
{
Console.WriteLine("Invalid API key format. OpenAI API keys should start with 'sk-'.");
return false;
}
// Prompt for endpoint and model
Console.Write($"Endpoint (default: https://api.openai.com/v1): ");
var endpointInput = Console.ReadLine()?.Trim();
var endpoint = string.IsNullOrWhiteSpace(endpointInput)
? "https://api.openai.com/v1"
: endpointInput;

Console.Write($"Default model (default: gpt-4o-mini): ");
var modelInput = Console.ReadLine()?.Trim();
var model = string.IsNullOrWhiteSpace(modelInput) ? "gpt-4o-mini" : modelInput;

// Load existing config or create new one
var config = await LoadConfigurationAsync() ?? new AppConfiguration();
config.OpenAiApiKey = apiKey;
config.OpenAiEndpoint = endpoint;
config.DefaultModel = model;

// Save configuration
await SaveConfigurationAsync(config);

Console.WriteLine($"✅ API key saved to {_configFilePath}");
Console.WriteLine($"✅ Configuration saved to {_configFilePath}");

if (verbose)
{
Expand All @@ -126,7 +149,7 @@ public async Task<bool> SetupApiKeyAsync(bool verbose = false)
Console.Write("Would you like to test the API key? (y/N): ");
var testResponse = Console.ReadLine()?.Trim().ToLowerInvariant();

if (testResponse == "y" || testResponse == "yes")
if ((testResponse == "y" || testResponse == "yes") && !string.IsNullOrEmpty(apiKey))
{
return await TestApiKeyAsync(apiKey, verbose);
}
Expand All @@ -137,7 +160,7 @@ public async Task<bool> SetupApiKeyAsync(bool verbose = false)
/// <summary>
/// Tests if the API key is valid by making a simple request
/// </summary>
private async Task<bool> TestApiKeyAsync(string apiKey, bool verbose)
private async Task<bool> TestApiKeyAsync(string? apiKey, bool verbose)
{
Console.WriteLine("Testing API key...");

Expand Down
19 changes: 16 additions & 3 deletions Services/OpenAIService.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Text;
using OpenAI.Chat;
using System.ClientModel;
using WriteCommit.Constants;
using WriteCommit.Models;

Expand All @@ -8,17 +9,21 @@ namespace WriteCommit.Services;
public class OpenAIService
{
private readonly string _apiKey;
private readonly string _endpoint;
private readonly string _patternsDirectory;
private const int MaxContextTokens = 128000;

public OpenAIService(string apiKey)
public OpenAIService(string apiKey, string? endpoint = null)
{
if (string.IsNullOrEmpty(apiKey))
{
throw new ArgumentNullException(nameof(apiKey), "API key cannot be null or empty");
}

_apiKey = apiKey;
_endpoint = string.IsNullOrWhiteSpace(endpoint)
? "https://api.openai.com/v1"
: endpoint;
_patternsDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "patterns");
}

Expand Down Expand Up @@ -150,7 +155,11 @@ bool verbose
}

// Create a client for this specific model
var chatClient = new ChatClient(model, _apiKey);
var clientOptions = new OpenAI.OpenAIClientOptions
{
Endpoint = new Uri(_endpoint)
};
var chatClient = new ChatClient(model, new ApiKeyCredential(_apiKey), clientOptions);

// Create the chat messages
var messages = new List<ChatMessage>
Expand Down Expand Up @@ -267,7 +276,11 @@ bool verbose
}

// Create a client for this specific model
var chatClient = new ChatClient(model, _apiKey);
var clientOptions = new OpenAI.OpenAIClientOptions
{
Endpoint = new Uri(_endpoint)
};
var chatClient = new ChatClient(model, new ApiKeyCredential(_apiKey), clientOptions);

// Create the chat messages
var messages = new List<ChatMessage>
Expand Down