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
5 changes: 3 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ PORT=80
IPS="420.69.247.365"
NODE_ENV="development"
OPENAI_API_KEY="DEEZ"
CLAUDE_API_KEY="NUTZ"
DEEPSEEK_API_KEY="BALLZ"
GEMINI_API_KEY="NUTZ"
CLAUDE_API_KEY="DEEZ"
DEEPSEEK_API_KEY="NUTZ"

# notify
NOTIFY_URL="https://notify.jaw.dev"
Expand Down
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ commit:
generate:
@git add -A && git --no-pager diff --cached | jq -Rs '{"diff": .}' | curl -s -X POST "http://localhost" -H "Content-Type: application/json" -d @- | jq -r '.message' && git reset -q

generate-openai:
@git add -A && git --no-pager diff --cached | jq -Rs '{"diff": ., "provider": "openai"}' | curl -s -X POST "http://localhost" -H "Content-Type: application/json" -d @- | jq -r '.message' && git reset -q

generate-claudeai:
@git add -A && git --no-pager diff --cached | jq -Rs '{"diff": ., "provider": "claudeai"}' | curl -s -X POST "http://localhost" -H "Content-Type: application/json" -d @- | jq -r '.message' && git reset -q

generate-deepseek:
@git add -A && git --no-pager diff --cached | jq -Rs '{"diff": ., "provider": "deepseek"}' | curl -s -X POST "http://localhost" -H "Content-Type: application/json" -d @- | jq -r '.message' && git reset -q

generate-gemini:
@git add -A && git --no-pager diff --cached | jq -Rs '{"diff": ., "provider": "gemini"}' | curl -s -X POST "http://localhost" -H "Content-Type: application/json" -d @- | jq -r '.message' && git reset -q

push:
@make format
@make lint
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ $ curl -s http://commit.jaw.dev/ | sh

### Options

- `-ai`, `--ai-provider` Specify AI provider (openai, claude, or deepseek, default: openai)
- `-ai`, `--ai-provider` Specify AI provider (openai, claudeai, deepseek, or gemini, default: openai)
- `-k`, `--api-key` Specify the API key for the AI provider
- `-dr`, `--dry-run` Run the script without making any changes
- `-nv`, `--no-verify` Skip message selection
Expand All @@ -51,8 +51,12 @@ $ curl -s http://commit.jaw.dev/ | sh -s -- --no-verify
$ curl -s http://commit.jaw.dev/ | sh -s -- --dry-run
$ curl -s http://commit.jaw.dev/ | sh -s -- -ai openai
$ curl -s http://commit.jaw.dev/ | sh -s -- -ai claudeai
$ curl -s http://commit.jaw.dev/ | sh -s -- -ai deepseek
$ curl -s http://commit.jaw.dev/ | sh -s -- -ai gemini
$ curl -s http://commit.jaw.dev/ | sh -s -- -ai openai --api-key YOUR_API_KEY
$ curl -s http://commit.jaw.dev/ | sh -s -- -ai claudeai --api-key YOUR_API_KEY
$ curl -s http://commit.jaw.dev/ | sh -s -- -ai deepseek --api-key YOUR_API_KEY
$ curl -s http://commit.jaw.dev/ | sh -s -- -ai gemini --api-key YOUR_API_KEY
$ curl -s http://commit.jaw.dev/ | sh -s -- -nv
$ curl -s http://commit.jaw.dev/ | sh -s -- -dr
$ curl -s http://commit.jaw.dev/ | sh -s -- -h
Expand Down
4 changes: 3 additions & 1 deletion src/ai.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from 'node:assert';
import { describe, it, mock } from 'node:test';
import { ai, openAI, claudeAI, prompt } from './ai';
import { ai, openAI, claudeAI, deepseekAI, geminiAI, prompt } from './ai';

describe('prompt', function () {
it('should return the expected prompt string', function () {
Expand Down Expand Up @@ -69,6 +69,8 @@ describe('ai()', function () {
it('should return the correct AI service based on the provider type', function () {
assert.strictEqual(ai('openai'), openAI);
assert.strictEqual(ai('claudeai'), claudeAI);
assert.strictEqual(ai('deepseek'), deepseekAI);
assert.strictEqual(ai('gemini'), geminiAI);
assert.strictEqual(ai(undefined), openAI);
});
});
Expand Down
59 changes: 59 additions & 0 deletions src/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
.filter((choice) => choice.message?.content)
.map((choice) => choice.message.content);
return getRandomElement(messages);
} catch (error: any) {

Check warning on line 98 in src/ai.ts

View workflow job for this annotation

GitHub Actions / ESLint (22.x)

Unexpected any. Specify a different type
if (error.code === 'insufficient_quota') {
throw new UnauthorizedError(
'You exceeded your current quota, please check your plan and billing details',
Expand Down Expand Up @@ -133,7 +133,7 @@
});
// @ts-ignore - trust me bro
return getRandomElement(messages.content).text;
} catch (error: any) {

Check warning on line 136 in src/ai.ts

View workflow job for this annotation

GitHub Actions / ESLint (22.x)

Unexpected any. Specify a different type
if (error?.error?.error?.type === 'authentication_error') {
throw new UnauthorizedError(error.error.error.message);
}
Expand Down Expand Up @@ -169,7 +169,7 @@
.filter((choice) => choice.message?.content)
.map((choice) => choice.message.content);
return getRandomElement(messages);
} catch (error: any) {

Check warning on line 172 in src/ai.ts

View workflow job for this annotation

GitHub Actions / ESLint (22.x)

Unexpected any. Specify a different type
if (error?.error?.type === 'invalid_api_key') {
throw new UnauthorizedError(error.message);
}
Expand All @@ -178,12 +178,71 @@
},
};

export const geminiAI: AIService = {
generate: async (diff: string, apiKey?: string) => {
try {
const API_KEY = apiKey ? apiKey : appConfig.GEMINI_API_KEY;
const response = await fetch(
'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify({
model: 'gemini-2.0-flash',
messages: [
{
role: 'system',
content: prompt,
},
{
role: 'user',
content: diff,
},
],
temperature: 0.7,
max_tokens: 200,
}),
},
);

if (!response.ok) {
const errorData = await response.json();
if (errorData.error?.status === 'UNAUTHENTICATED') {
throw new UnauthorizedError('Invalid API key or authentication error');
}
if (errorData.error?.status === 'RESOURCE_EXHAUSTED') {
throw new UnauthorizedError(
'You exceeded your current quota, please check your plan and billing details',
);
}
throw new Error(errorData.error?.message || 'Error calling Gemini API');
}

const data = await response.json();
const messages = data.choices
.filter((choice: any) => choice.message?.content)

Check warning on line 226 in src/ai.ts

View workflow job for this annotation

GitHub Actions / ESLint (22.x)

Unexpected any. Specify a different type
.map((choice: any) => choice.message.content);

Check warning on line 227 in src/ai.ts

View workflow job for this annotation

GitHub Actions / ESLint (22.x)

Unexpected any. Specify a different type
return getRandomElement(messages);
} catch (error: any) {

Check warning on line 229 in src/ai.ts

View workflow job for this annotation

GitHub Actions / ESLint (22.x)

Unexpected any. Specify a different type
if (error instanceof UnauthorizedError) {
throw error;
}
throw error;
}
},
};

export function ai(type?: Provider): AIService {
switch (type) {
case 'claudeai':
return claudeAI;
case 'deepseek':
return deepseekAI;
case 'gemini':
return geminiAI;
case 'openai':
default:
return openAI;
Expand Down
10 changes: 6 additions & 4 deletions src/commit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ show_help() {
printf "${YELLOW}Options:${NC}\n"
printf " ${GREEN}-dr, --dry-run${NC} Run the script without making any changes\n"
printf " ${GREEN}-nv, --no-verify${NC} Skip message selection\n"
printf " ${GREEN}-ai, --ai-provider${NC} Specify AI provider (openai, claude or deepseek, default: openai)\n"
printf " ${GREEN}-ai, --ai-provider${NC} Specify AI provider (openai, claudeai, deepseek, or gemini, default: openai)\n"
printf " ${GREEN}-k, --api-key${NC} Specify the API key for the AI provider\n"
printf " ${GREEN}-v, --verbose${NC} Enable verbose logging\n"
printf " ${GREEN}-h, --help${NC} Display this help message\n"
Expand All @@ -45,7 +45,9 @@ show_help() {
printf " ${GREEN}Dry run:${NC}\n"
printf " curl -s http://localhost | sh -s -- --dry-run\n"
printf " ${GREEN}Use Claude AI with API key:${NC}\n"
printf " curl -s http://localhost | sh -s -- --ai-provider claude --api-key YOUR_API_KEY\n"
printf " curl -s http://localhost | sh -s -- --ai-provider claudeai --api-key YOUR_API_KEY\n"
printf " ${GREEN}Use Gemini AI with API key:${NC}\n"
printf " curl -s http://localhost | sh -s -- --ai-provider gemini --api-key YOUR_API_KEY\n"
printf " ${GREEN}Enable verbose logging:${NC}\n"
printf " curl -s http://localhost | sh -s -- --verbose\n"
printf "\n"
Expand All @@ -71,9 +73,9 @@ parse_arguments() {
-ai|--ai-provider)
AI_PROVIDER=$2
log_verbose "AI provider set to: " "$AI_PROVIDER"
if [[ "$AI_PROVIDER" != "openai" && "$AI_PROVIDER" != "claudeai" && "$AI_PROVIDER" != "deepseek" ]]; then
if [[ "$AI_PROVIDER" != "openai" && "$AI_PROVIDER" != "claudeai" && "$AI_PROVIDER" != "deepseek" && "$AI_PROVIDER" != "gemini" ]]; then
log_verbose "Invalid AI provider specified"
echo -e "${RED}Invalid AI provider. Please use 'openai' or 'claudeai'.${NC}\n"
echo -e "${RED}Invalid AI provider. Please use 'openai', 'claudeai', 'deepseek', or 'gemini'.${NC}\n"
exit 1
fi
shift 2
Expand Down
5 changes: 5 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,22 @@
NOTIFY_URL: {
value: process.env.NOTIFY_URL,
required: true,
type: (value: any) => String(value),

Check warning on line 11 in src/config.ts

View workflow job for this annotation

GitHub Actions / ESLint (22.x)

Unexpected any. Specify a different type
},
NOTIFY_X_API_KEY: {
value: process.env.NOTIFY_X_API_KEY,
required: true,
type: (value: any) => String(value),

Check warning on line 16 in src/config.ts

View workflow job for this annotation

GitHub Actions / ESLint (22.x)

Unexpected any. Specify a different type
},
IPS: {
value: process.env.IPS,
required: true,
type: (value: any) => String(value),

Check warning on line 21 in src/config.ts

View workflow job for this annotation

GitHub Actions / ESLint (22.x)

Unexpected any. Specify a different type
},
PORT: {
value: process.env.PORT,
default: 80,
type: (value: any) => Number(value),

Check warning on line 26 in src/config.ts

View workflow job for this annotation

GitHub Actions / ESLint (22.x)

Unexpected any. Specify a different type
required: false,
},
OPENAI_API_KEY: {
Expand All @@ -47,4 +47,9 @@
required: true,
type: (value: any) => String(value),
},
GEMINI_API_KEY: {
value: process.env.GEMINI_API_KEY,
required: true,
type: (value: any) => String(value),
},
});
8 changes: 7 additions & 1 deletion src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,13 @@ export function postGenerateCommitMessageHandler(ai: (type?: Provider) => AIServ
throw new ValidationError('Diff must not be empty!');
}

if (provider && provider !== 'openai' && provider !== 'claudeai' && provider !== 'deepseek') {
if (
provider &&
provider !== 'openai' &&
provider !== 'claudeai' &&
provider !== 'deepseek' &&
provider !== 'gemini'
) {
throw new ValidationError('Invalid provider specified!');
}

Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Request } from 'express';

export type Provider = 'openai' | 'claudeai' | 'deepseek';
export type Provider = 'openai' | 'claudeai' | 'deepseek' | 'gemini';

export interface GenerateCommitMessageRequest extends Request {
body: {
Expand Down