Skip to content
Open
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
26 changes: 26 additions & 0 deletions .github/workflows/publish-extension.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Publish VS Code Extension

on:
workflow_dispatch:
push:
tags:
- 'extension-v*'

jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: |
cd vscode-extension
npm ci
npm run build
- name: Install vsce
run: npm install -g vsce
- name: Publish to Marketplace
run: vsce publish -p ${{ secrets.VSCE_TOKEN }}
working-directory: vscode-extension
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,7 @@ Thumbs.db

# Fabric temporary files
fabric_temp_*
vscode-extension/node_modules
vscode-extension/out
vscode-extension/package-lock.json
vscode-extension/*.vsix
15 changes: 0 additions & 15 deletions Constants/FabricPatterns.cs

This file was deleted.

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
56 changes: 37 additions & 19 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 @@ -77,19 +77,16 @@ WriteCommit

```bash
# Preview message without committing
write-commit --dry-run
WriteCommit --dry-run

# Detailed output for debugging
write-commit --verbose
WriteCommit --verbose

# Custom AI parameters
write-commit --temperature 0.7 --topp 0.9 --pattern custom_pattern

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

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

## ⚙️ Configuration Options
Expand All @@ -98,18 +95,16 @@ write-commit --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 @@ -122,22 +117,22 @@ write-commit --dry-run --verbose --temperature 0.5 --reinstall-patterns

```bash
# Run the setup wizard
write-commit --setup
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 All @@ -154,6 +149,29 @@ dotnet build
dotnet run -- --help
```

## VS Code Extension

The `vscode-extension` folder contains an extension that lets you run WriteCommit
directly from VS Code. To try it out:

1. Open the `vscode-extension` folder in VS Code.
2. Run `npm install` and then `npm run build` to compile the extension.
3. Press `F5` to launch an Extension Development Host.

The extension adds a Source Control panel action that runs `WriteCommit --dry-run`
and inserts the generated message into the commit input box. As soon as the
extension activates it ensures the CLI is installed, reusing the endpoint and
model you have configured through the `vscode-lm` Language Model settings (for
example GitHub Copilot). You can still override those values through the
extension settings, which also let you configure the OpenAI API key and the CLI
path.

### Publishing
The workflow `.github/workflows/publish-extension.yml` uses `vsce publish` to
upload the extension to the Visual Studio Code Marketplace when a tag matching
`extension-v*` is pushed. Before triggering the workflow, add a `VSCE_TOKEN`
secret to your repository with a Marketplace personal access token.

### Contributing
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
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
Loading