Compare commits

...

28 Commits
v3.2.8 ... dev

Author SHA1 Message Date
di-sukharev
8ae2f7ddf1 3.2.10 2025-08-01 16:05:20 +03:00
di-sukharev
b318d1d882 Merge branch 'master' into dev 2025-08-01 16:02:44 +03:00
GPT8
af0f2c1df4 Merge pull request #505 from D1m7asis/dev-aimlapi
feat: add AIML API provider support
2025-08-01 16:00:16 +03:00
D1m7asis
c5ce50aaa3 feat: add AIML API provider support
Introduces AIMLAPI as a supported AI provider, including model list, config validation, and engine implementation. Updates README and engine selection logic to integrate AIMLAPI for chat completions.

Refactor AimlApiEngine response handling

Removed dependency on removeContentTags and simplified message content extraction. Minor header formatting fix for HTTP-Referer. This streamlines the response handling and reduces unnecessary processing.
2025-08-01 14:48:11 +02:00
GPT8
c1756b85af Merge pull request #498 from kykungz/fix-491
Fix TypeScript build error and add missing confirm import (regression from #491)
2025-07-23 17:12:44 +03:00
GPT8
dac1271782 Merge pull request #496 from kykungz/resolve-top-level-git-dir
Fix git commands when executed from subdirectories
2025-07-23 17:10:37 +03:00
Kongpon Charanwattanakit
1cc7a64f99 feat(commit.ts): add confirmation prompt and refactor commit message editing for better user experience 2025-07-23 16:15:20 +07:00
GPT8
4deb7bca65 Merge pull request #488 from anpigon/fix/i18n-ko
fix(i18n): correct typo in Korean translation for 'feat' commit type
2025-07-22 23:40:54 +03:00
GPT8
1a90485a10 Merge pull request #491 from leoliu0605/dev
feat(commit.ts): enable users to edit commit message before committing
2025-07-22 23:38:30 +03:00
GPT8
48b8d9d7b2 Merge pull request #494 from PhantasWeng/commit-hook-default
feat(config): add OCO_HOOK_AUTO_UNCOMMENT config key and update commit message hook behavior to conditionally uncomment the message
2025-07-22 23:37:05 +03:00
Kongpon Charanwattanakit
7e60c68ba5 refactor(git): add getGitDir helper and update functions to use cwd option for better git repository handling 2025-07-14 21:50:58 +07:00
Phantas Weng
24adc16adf fix(run.ts): remove trailing comma from OCO_AI_PROVIDER_ENUM array to fix the prettier test 2025-07-08 09:27:40 +00:00
Phantas Weng
881f07eebe fix(prepare-commit-msg-hook): simplify commit message generation logic for clarity and maintainability 2025-07-08 05:38:42 +00:00
Phantas Weng
3a255a3ad9 feat(config): add OCO_HOOK_AUTO_UNCOMMENT config key and update commit message hook behavior to conditionally uncomment the message 2025-07-08 05:25:32 +00:00
GPT8
9971b3c74e Merge pull request #492 from PhantasWeng/git-hook-message
feat(prepare-commit-msg-hook): enhance commit message formatting with a divider and instructions for better user guidance
2025-07-04 11:42:30 +03:00
Phantas Weng
66a5695d89 feat(prepare-commit-msg-hook): enhance commit message formatting with a divider and instructions for better user guidance 2025-07-01 06:02:32 +00:00
GPT8
fd22f713ed Merge pull request #489 from yshngg/patch-1
fix(migrations): skip unhandled AI providers during migration execution
2025-06-29 12:19:17 +03:00
leoliu
43dc5e6c2b feat(commit.ts): enable users to edit commit message before committing 2025-06-26 23:41:58 +08:00
Yusheng Guo
3d42dde48c fix(migrations): skip unhandled AI providers during migration execution
The changes:
1. Expanded the skip condition to include additional AI providers (DEEPSEEK, GROQ, MISTRAL, MLX, OPENROUTER) beyond just TEST
2. Maintained existing TEST provider skip behavior
3. Added explicit comment explaining the skip logic

The why:
Prevents migration execution for unsupported AI providers to avoid potential runtime errors or data inconsistencies, ensuring migrations only run for properly handled configurations.
2025-06-23 15:34:22 +08:00
anpigon
19f32ca57d fix(i18n): correct typo in Korean translation for 'feat' commit type #487 2025-06-21 18:12:55 +09:00
GPT8
c1070789fd Merge pull request #485 from frauniki/add-prettier-ci
chore: Add Prettier format check to CI and format code
2025-06-15 12:18:31 +03:00
di-sukharev
1f0f44ede0 build 2025-06-15 12:17:18 +03:00
di-sukharev
48cdcbceb2 3.2.9 2025-06-15 12:17:16 +03:00
frauniki
45aed936b1 ♻️ refactor: clean up code formatting and improve readability
- Fix inconsistent indentation across multiple engine files
- Remove trailing whitespace and add missing newlines
- Improve code formatting in prompt generation functions
- Break long lines for better readability
- Standardize spacing and brackets placement
2025-06-15 17:29:12 +09:00
frauniki
e4f7e8dc80 add prettier formatting check to CI workflow and npm scripts
- Add prettier job to GitHub Actions workflow to enforce code formatting
- Add format:check script to package.json for checking formatting
- Include failure message when prettier check fails in CI
2025-06-15 17:28:40 +09:00
GPT8
15ac076aed Merge pull request #484 from frauniki/add-openrouter-engine
 add openrouter AI provider support with comprehensive model list
2025-06-15 10:28:46 +03:00
frauniki
5725c776a7 add openrouter AI provider support with comprehensive model list
Add OpenRouterEngine class and integrate it into the configuration
system. OpenRouter provides access to 300+ AI models through a
unified API, expanding model availability for commit message
generation beyond existing providers.
2025-06-15 04:11:13 +09:00
di-sukharev
6f541d33cc build 2025-06-08 11:17:09 +03:00
26 changed files with 1609 additions and 189 deletions

View File

@@ -51,3 +51,21 @@ jobs:
run: npm run build
- name: Run E2E Tests
run: npm run test:e2e
prettier:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run Prettier
run: npm run format:check
- name: Prettier Output
if: failure()
run: |
echo "Prettier check failed. Please run 'npm run format' to fix formatting issues."
exit 1

View File

@@ -106,7 +106,7 @@ Create a `.env` file and add OpenCommit config variables there like this:
```env
...
OCO_AI_PROVIDER=<openai (default), anthropic, azure, ollama, gemini, flowise, deepseek>
OCO_AI_PROVIDER=<openai (default), anthropic, azure, ollama, gemini, flowise, deepseek, aimlapi>
OCO_API_KEY=<your OpenAI API token> // or other LLM provider API token
OCO_API_URL=<may be used to set proxy path to OpenAI api>
OCO_API_CUSTOM_HEADERS=<JSON string of custom HTTP headers to include in API requests>

View File

@@ -47672,7 +47672,7 @@ function G3(t2, e3) {
// package.json
var package_default = {
name: "opencommit",
version: "3.2.7",
version: "3.2.9",
description: "Auto-generate impressive commits in 1 second. Killing lame commits with AI \u{1F92F}\u{1F52B}",
keywords: [
"git",
@@ -50371,7 +50371,338 @@ var MODEL_LIST = {
"mistral-moderation-2411",
"mistral-moderation-latest"
],
deepseek: ["deepseek-chat", "deepseek-reasoner"]
deepseek: ["deepseek-chat", "deepseek-reasoner"],
// OpenRouter available models
// input_modalities: 'text'
// output_modalities: 'text'
// https://openrouter.ai/api/v1/models
openrouter: [
"openai/gpt-4o-mini",
// used by default
"01-ai/yi-large",
"aetherwiing/mn-starcannon-12b",
"agentica-org/deepcoder-14b-preview:free",
"ai21/jamba-1.6-large",
"ai21/jamba-1.6-mini",
"aion-labs/aion-1.0",
"aion-labs/aion-1.0-mini",
"aion-labs/aion-rp-llama-3.1-8b",
"alfredpros/codellama-7b-instruct-solidity",
"all-hands/openhands-lm-32b-v0.1",
"alpindale/goliath-120b",
"alpindale/magnum-72b",
"amazon/nova-lite-v1",
"amazon/nova-micro-v1",
"amazon/nova-pro-v1",
"anthracite-org/magnum-v2-72b",
"anthracite-org/magnum-v4-72b",
"anthropic/claude-2",
"anthropic/claude-2.0",
"anthropic/claude-2.0:beta",
"anthropic/claude-2.1",
"anthropic/claude-2.1:beta",
"anthropic/claude-2:beta",
"anthropic/claude-3-haiku",
"anthropic/claude-3-haiku:beta",
"anthropic/claude-3-opus",
"anthropic/claude-3-opus:beta",
"anthropic/claude-3-sonnet",
"anthropic/claude-3-sonnet:beta",
"anthropic/claude-3.5-haiku",
"anthropic/claude-3.5-haiku-20241022",
"anthropic/claude-3.5-haiku-20241022:beta",
"anthropic/claude-3.5-haiku:beta",
"anthropic/claude-3.5-sonnet",
"anthropic/claude-3.5-sonnet-20240620",
"anthropic/claude-3.5-sonnet-20240620:beta",
"anthropic/claude-3.5-sonnet:beta",
"anthropic/claude-3.7-sonnet",
"anthropic/claude-3.7-sonnet:beta",
"anthropic/claude-3.7-sonnet:thinking",
"anthropic/claude-opus-4",
"anthropic/claude-sonnet-4",
"arcee-ai/arcee-blitz",
"arcee-ai/caller-large",
"arcee-ai/coder-large",
"arcee-ai/maestro-reasoning",
"arcee-ai/spotlight",
"arcee-ai/virtuoso-large",
"arcee-ai/virtuoso-medium-v2",
"arliai/qwq-32b-arliai-rpr-v1:free",
"cognitivecomputations/dolphin-mixtral-8x22b",
"cognitivecomputations/dolphin3.0-mistral-24b:free",
"cognitivecomputations/dolphin3.0-r1-mistral-24b:free",
"cohere/command",
"cohere/command-a",
"cohere/command-r",
"cohere/command-r-03-2024",
"cohere/command-r-08-2024",
"cohere/command-r-plus",
"cohere/command-r-plus-04-2024",
"cohere/command-r-plus-08-2024",
"cohere/command-r7b-12-2024",
"deepseek/deepseek-chat",
"deepseek/deepseek-chat-v3-0324",
"deepseek/deepseek-chat-v3-0324:free",
"deepseek/deepseek-chat:free",
"deepseek/deepseek-prover-v2",
"deepseek/deepseek-prover-v2:free",
"deepseek/deepseek-r1",
"deepseek/deepseek-r1-0528",
"deepseek/deepseek-r1-0528-qwen3-8b",
"deepseek/deepseek-r1-0528-qwen3-8b:free",
"deepseek/deepseek-r1-0528:free",
"deepseek/deepseek-r1-distill-llama-70b",
"deepseek/deepseek-r1-distill-llama-70b:free",
"deepseek/deepseek-r1-distill-llama-8b",
"deepseek/deepseek-r1-distill-qwen-1.5b",
"deepseek/deepseek-r1-distill-qwen-14b",
"deepseek/deepseek-r1-distill-qwen-14b:free",
"deepseek/deepseek-r1-distill-qwen-32b",
"deepseek/deepseek-r1-distill-qwen-32b:free",
"deepseek/deepseek-r1-distill-qwen-7b",
"deepseek/deepseek-r1-zero:free",
"deepseek/deepseek-r1:free",
"deepseek/deepseek-v3-base:free",
"eleutherai/llemma_7b",
"eva-unit-01/eva-llama-3.33-70b",
"eva-unit-01/eva-qwen-2.5-32b",
"eva-unit-01/eva-qwen-2.5-72b",
"featherless/qwerky-72b:free",
"google/gemini-2.0-flash-001",
"google/gemini-2.0-flash-exp:free",
"google/gemini-2.0-flash-lite-001",
"google/gemini-2.5-flash-preview",
"google/gemini-2.5-flash-preview-05-20",
"google/gemini-2.5-flash-preview-05-20:thinking",
"google/gemini-2.5-flash-preview:thinking",
"google/gemini-2.5-pro-exp-03-25",
"google/gemini-2.5-pro-preview",
"google/gemini-2.5-pro-preview-05-06",
"google/gemini-flash-1.5",
"google/gemini-flash-1.5-8b",
"google/gemini-pro-1.5",
"google/gemma-2-27b-it",
"google/gemma-2-9b-it",
"google/gemma-2-9b-it:free",
"google/gemma-3-12b-it",
"google/gemma-3-12b-it:free",
"google/gemma-3-1b-it:free",
"google/gemma-3-27b-it",
"google/gemma-3-27b-it:free",
"google/gemma-3-4b-it",
"google/gemma-3-4b-it:free",
"google/gemma-3n-e4b-it:free",
"gryphe/mythomax-l2-13b",
"inception/mercury-coder-small-beta",
"infermatic/mn-inferor-12b",
"inflection/inflection-3-pi",
"inflection/inflection-3-productivity",
"liquid/lfm-3b",
"liquid/lfm-40b",
"liquid/lfm-7b",
"mancer/weaver",
"meta-llama/llama-2-70b-chat",
"meta-llama/llama-3-70b-instruct",
"meta-llama/llama-3-8b-instruct",
"meta-llama/llama-3.1-405b",
"meta-llama/llama-3.1-405b-instruct",
"meta-llama/llama-3.1-405b:free",
"meta-llama/llama-3.1-70b-instruct",
"meta-llama/llama-3.1-8b-instruct",
"meta-llama/llama-3.1-8b-instruct:free",
"meta-llama/llama-3.2-11b-vision-instruct",
"meta-llama/llama-3.2-11b-vision-instruct:free",
"meta-llama/llama-3.2-1b-instruct",
"meta-llama/llama-3.2-1b-instruct:free",
"meta-llama/llama-3.2-3b-instruct",
"meta-llama/llama-3.2-3b-instruct:free",
"meta-llama/llama-3.2-90b-vision-instruct",
"meta-llama/llama-3.3-70b-instruct",
"meta-llama/llama-3.3-70b-instruct:free",
"meta-llama/llama-3.3-8b-instruct:free",
"meta-llama/llama-4-maverick",
"meta-llama/llama-4-maverick:free",
"meta-llama/llama-4-scout",
"meta-llama/llama-4-scout:free",
"meta-llama/llama-guard-2-8b",
"meta-llama/llama-guard-3-8b",
"meta-llama/llama-guard-4-12b",
"microsoft/mai-ds-r1:free",
"microsoft/phi-3-medium-128k-instruct",
"microsoft/phi-3-mini-128k-instruct",
"microsoft/phi-3.5-mini-128k-instruct",
"microsoft/phi-4",
"microsoft/phi-4-multimodal-instruct",
"microsoft/phi-4-reasoning-plus",
"microsoft/phi-4-reasoning-plus:free",
"microsoft/phi-4-reasoning:free",
"microsoft/wizardlm-2-8x22b",
"minimax/minimax-01",
"mistralai/codestral-2501",
"mistralai/devstral-small",
"mistralai/devstral-small:free",
"mistralai/magistral-medium-2506",
"mistralai/magistral-medium-2506:thinking",
"mistralai/magistral-small-2506",
"mistralai/ministral-3b",
"mistralai/ministral-8b",
"mistralai/mistral-7b-instruct",
"mistralai/mistral-7b-instruct-v0.1",
"mistralai/mistral-7b-instruct-v0.2",
"mistralai/mistral-7b-instruct-v0.3",
"mistralai/mistral-7b-instruct:free",
"mistralai/mistral-large",
"mistralai/mistral-large-2407",
"mistralai/mistral-large-2411",
"mistralai/mistral-medium",
"mistralai/mistral-medium-3",
"mistralai/mistral-nemo",
"mistralai/mistral-nemo:free",
"mistralai/mistral-saba",
"mistralai/mistral-small",
"mistralai/mistral-small-24b-instruct-2501",
"mistralai/mistral-small-24b-instruct-2501:free",
"mistralai/mistral-small-3.1-24b-instruct",
"mistralai/mistral-small-3.1-24b-instruct:free",
"mistralai/mistral-tiny",
"mistralai/mixtral-8x22b-instruct",
"mistralai/mixtral-8x7b-instruct",
"mistralai/pixtral-12b",
"mistralai/pixtral-large-2411",
"moonshotai/kimi-vl-a3b-thinking:free",
"moonshotai/moonlight-16b-a3b-instruct:free",
"neversleep/llama-3-lumimaid-70b",
"neversleep/llama-3-lumimaid-8b",
"neversleep/llama-3.1-lumimaid-70b",
"neversleep/llama-3.1-lumimaid-8b",
"neversleep/noromaid-20b",
"nothingiisreal/mn-celeste-12b",
"nousresearch/deephermes-3-llama-3-8b-preview:free",
"nousresearch/deephermes-3-mistral-24b-preview:free",
"nousresearch/hermes-2-pro-llama-3-8b",
"nousresearch/hermes-3-llama-3.1-405b",
"nousresearch/hermes-3-llama-3.1-70b",
"nousresearch/nous-hermes-2-mixtral-8x7b-dpo",
"nvidia/llama-3.1-nemotron-70b-instruct",
"nvidia/llama-3.1-nemotron-ultra-253b-v1",
"nvidia/llama-3.1-nemotron-ultra-253b-v1:free",
"nvidia/llama-3.3-nemotron-super-49b-v1",
"nvidia/llama-3.3-nemotron-super-49b-v1:free",
"open-r1/olympiccoder-32b:free",
"openai/chatgpt-4o-latest",
"openai/codex-mini",
"openai/gpt-3.5-turbo",
"openai/gpt-3.5-turbo-0125",
"openai/gpt-3.5-turbo-0613",
"openai/gpt-3.5-turbo-1106",
"openai/gpt-3.5-turbo-16k",
"openai/gpt-3.5-turbo-instruct",
"openai/gpt-4",
"openai/gpt-4-0314",
"openai/gpt-4-1106-preview",
"openai/gpt-4-turbo",
"openai/gpt-4-turbo-preview",
"openai/gpt-4.1",
"openai/gpt-4.1-mini",
"openai/gpt-4.1-nano",
"openai/gpt-4.5-preview",
"openai/gpt-4o",
"openai/gpt-4o-2024-05-13",
"openai/gpt-4o-2024-08-06",
"openai/gpt-4o-2024-11-20",
"openai/gpt-4o-mini-2024-07-18",
"openai/gpt-4o-mini-search-preview",
"openai/gpt-4o-search-preview",
"openai/gpt-4o:extended",
"openai/o1",
"openai/o1-mini",
"openai/o1-mini-2024-09-12",
"openai/o1-preview",
"openai/o1-preview-2024-09-12",
"openai/o1-pro",
"openai/o3",
"openai/o3-mini",
"openai/o3-mini-high",
"openai/o3-pro",
"openai/o4-mini",
"openai/o4-mini-high",
"opengvlab/internvl3-14b:free",
"opengvlab/internvl3-2b:free",
"openrouter/auto",
"perplexity/llama-3.1-sonar-large-128k-online",
"perplexity/llama-3.1-sonar-small-128k-online",
"perplexity/r1-1776",
"perplexity/sonar",
"perplexity/sonar-deep-research",
"perplexity/sonar-pro",
"perplexity/sonar-reasoning",
"perplexity/sonar-reasoning-pro",
"pygmalionai/mythalion-13b",
"qwen/qwen-2-72b-instruct",
"qwen/qwen-2.5-72b-instruct",
"qwen/qwen-2.5-72b-instruct:free",
"qwen/qwen-2.5-7b-instruct",
"qwen/qwen-2.5-7b-instruct:free",
"qwen/qwen-2.5-coder-32b-instruct",
"qwen/qwen-2.5-coder-32b-instruct:free",
"qwen/qwen-2.5-vl-7b-instruct",
"qwen/qwen-2.5-vl-7b-instruct:free",
"qwen/qwen-max",
"qwen/qwen-plus",
"qwen/qwen-turbo",
"qwen/qwen-vl-max",
"qwen/qwen-vl-plus",
"qwen/qwen2.5-vl-32b-instruct",
"qwen/qwen2.5-vl-32b-instruct:free",
"qwen/qwen2.5-vl-3b-instruct:free",
"qwen/qwen2.5-vl-72b-instruct",
"qwen/qwen2.5-vl-72b-instruct:free",
"qwen/qwen3-14b",
"qwen/qwen3-14b:free",
"qwen/qwen3-235b-a22b",
"qwen/qwen3-235b-a22b:free",
"qwen/qwen3-30b-a3b",
"qwen/qwen3-30b-a3b:free",
"qwen/qwen3-32b",
"qwen/qwen3-32b:free",
"qwen/qwen3-8b",
"qwen/qwen3-8b:free",
"qwen/qwq-32b",
"qwen/qwq-32b-preview",
"qwen/qwq-32b:free",
"raifle/sorcererlm-8x22b",
"rekaai/reka-flash-3:free",
"sao10k/fimbulvetr-11b-v2",
"sao10k/l3-euryale-70b",
"sao10k/l3-lunaris-8b",
"sao10k/l3.1-euryale-70b",
"sao10k/l3.3-euryale-70b",
"sarvamai/sarvam-m:free",
"scb10x/llama3.1-typhoon2-70b-instruct",
"sentientagi/dobby-mini-unhinged-plus-llama-3.1-8b",
"shisa-ai/shisa-v2-llama3.3-70b:free",
"sophosympatheia/midnight-rose-70b",
"thedrummer/anubis-pro-105b-v1",
"thedrummer/rocinante-12b",
"thedrummer/skyfall-36b-v2",
"thedrummer/unslopnemo-12b",
"thedrummer/valkyrie-49b-v1",
"thudm/glm-4-32b",
"thudm/glm-4-32b:free",
"thudm/glm-z1-32b",
"thudm/glm-z1-32b:free",
"thudm/glm-z1-rumination-32b",
"tngtech/deepseek-r1t-chimera:free",
"undi95/remm-slerp-l2-13b",
"undi95/toppy-m-7b",
"x-ai/grok-2-1212",
"x-ai/grok-2-vision-1212",
"x-ai/grok-3-beta",
"x-ai/grok-3-mini-beta",
"x-ai/grok-beta",
"x-ai/grok-vision-beta"
]
};
var getDefaultModel = (provider) => {
switch (provider) {
@@ -50389,6 +50720,8 @@ var getDefaultModel = (provider) => {
return MODEL_LIST.mistral[0];
case "deepseek":
return MODEL_LIST.deepseek[0];
case "openrouter":
return MODEL_LIST.openrouter[0];
default:
return MODEL_LIST.openai[0];
}
@@ -50542,7 +50875,8 @@ var configValidators = {
"test",
"flowise",
"groq",
"deepseek"
"deepseek",
"openrouter"
].includes(value) || value.startsWith("ollama"),
`${value} is not supported yet, use 'ollama', 'mlx', 'anthropic', 'azure', 'gemini', 'flowise', 'mistral', 'deepseek' or 'openai' (default)`
);
@@ -50587,6 +50921,7 @@ var OCO_AI_PROVIDER_ENUM = /* @__PURE__ */ ((OCO_AI_PROVIDER_ENUM2) => {
OCO_AI_PROVIDER_ENUM2["MISTRAL"] = "mistral";
OCO_AI_PROVIDER_ENUM2["MLX"] = "mlx";
OCO_AI_PROVIDER_ENUM2["DEEPSEEK"] = "deepseek";
OCO_AI_PROVIDER_ENUM2["OPENROUTER"] = "openrouter";
return OCO_AI_PROVIDER_ENUM2;
})(OCO_AI_PROVIDER_ENUM || {});
var defaultConfigPath = (0, import_path.join)((0, import_os.homedir)(), ".opencommit");
@@ -50874,7 +51209,11 @@ ${key}:`));
}
}
}
console.log(source_default.yellow('\nUse "oco config describe [PARAMETER]" to see accepted values and more details for a specific config parameter.'));
console.log(
source_default.yellow(
'\nUse "oco config describe [PARAMETER]" to see accepted values and more details for a specific config parameter.'
)
);
}
var configCommand = G3(
{
@@ -66182,6 +66521,40 @@ var DeepseekEngine = class extends OpenAiEngine {
}
};
// src/engine/openrouter.ts
var OpenRouterEngine = class {
constructor(config7) {
this.config = config7;
this.generateCommitMessage = async (messages) => {
try {
const response = await this.client.post("", {
model: this.config.model,
messages
});
const message = response.data.choices[0].message;
let content = message?.content;
return removeContentTags(content, "think");
} catch (error) {
const err = error;
if (axios_default.isAxiosError(error) && error.response?.status === 401) {
const openRouterError = error.response.data.error;
if (openRouterError) throw new Error(openRouterError.message);
}
throw err;
}
};
this.client = axios_default.create({
baseURL: "https://openrouter.ai/api/v1/chat/completions",
headers: {
Authorization: `Bearer ${config7.apiKey}`,
"HTTP-Referer": "https://github.com/di-sukharev/opencommit",
"X-Title": "OpenCommit",
"Content-Type": "application/json"
}
});
}
};
// src/utils/engine.ts
function parseCustomHeaders(headers) {
let parsedHeaders = {};
@@ -66195,7 +66568,9 @@ function parseCustomHeaders(headers) {
parsedHeaders = JSON.parse(headers);
}
} catch (error) {
console.warn("Invalid OCO_API_CUSTOM_HEADERS format, ignoring custom headers");
console.warn(
"Invalid OCO_API_CUSTOM_HEADERS format, ignoring custom headers"
);
}
return parsedHeaders;
}
@@ -66232,6 +66607,8 @@ function getEngine() {
return new MLXEngine(DEFAULT_CONFIG2);
case "deepseek" /* DEEPSEEK */:
return new DeepseekEngine(DEFAULT_CONFIG2);
case "openrouter" /* OPENROUTER */:
return new OpenRouterEngine(DEFAULT_CONFIG2);
default:
return new OpenAiEngine(DEFAULT_CONFIG2);
}
@@ -66936,7 +67313,7 @@ var gitAdd = async ({ files }) => {
const gitAddSpinner = le();
gitAddSpinner.start("Adding files to commit");
await execa("git", ["add", ...files]);
gitAddSpinner.stop(`Done adding ${files.length} files`);
gitAddSpinner.stop(`Staged ${files.length} files`);
};
var getDiff = async ({ files }) => {
const lockFiles = files.filter(

View File

@@ -70984,7 +70984,338 @@ var MODEL_LIST = {
"mistral-moderation-2411",
"mistral-moderation-latest"
],
deepseek: ["deepseek-chat", "deepseek-reasoner"]
deepseek: ["deepseek-chat", "deepseek-reasoner"],
// OpenRouter available models
// input_modalities: 'text'
// output_modalities: 'text'
// https://openrouter.ai/api/v1/models
openrouter: [
"openai/gpt-4o-mini",
// used by default
"01-ai/yi-large",
"aetherwiing/mn-starcannon-12b",
"agentica-org/deepcoder-14b-preview:free",
"ai21/jamba-1.6-large",
"ai21/jamba-1.6-mini",
"aion-labs/aion-1.0",
"aion-labs/aion-1.0-mini",
"aion-labs/aion-rp-llama-3.1-8b",
"alfredpros/codellama-7b-instruct-solidity",
"all-hands/openhands-lm-32b-v0.1",
"alpindale/goliath-120b",
"alpindale/magnum-72b",
"amazon/nova-lite-v1",
"amazon/nova-micro-v1",
"amazon/nova-pro-v1",
"anthracite-org/magnum-v2-72b",
"anthracite-org/magnum-v4-72b",
"anthropic/claude-2",
"anthropic/claude-2.0",
"anthropic/claude-2.0:beta",
"anthropic/claude-2.1",
"anthropic/claude-2.1:beta",
"anthropic/claude-2:beta",
"anthropic/claude-3-haiku",
"anthropic/claude-3-haiku:beta",
"anthropic/claude-3-opus",
"anthropic/claude-3-opus:beta",
"anthropic/claude-3-sonnet",
"anthropic/claude-3-sonnet:beta",
"anthropic/claude-3.5-haiku",
"anthropic/claude-3.5-haiku-20241022",
"anthropic/claude-3.5-haiku-20241022:beta",
"anthropic/claude-3.5-haiku:beta",
"anthropic/claude-3.5-sonnet",
"anthropic/claude-3.5-sonnet-20240620",
"anthropic/claude-3.5-sonnet-20240620:beta",
"anthropic/claude-3.5-sonnet:beta",
"anthropic/claude-3.7-sonnet",
"anthropic/claude-3.7-sonnet:beta",
"anthropic/claude-3.7-sonnet:thinking",
"anthropic/claude-opus-4",
"anthropic/claude-sonnet-4",
"arcee-ai/arcee-blitz",
"arcee-ai/caller-large",
"arcee-ai/coder-large",
"arcee-ai/maestro-reasoning",
"arcee-ai/spotlight",
"arcee-ai/virtuoso-large",
"arcee-ai/virtuoso-medium-v2",
"arliai/qwq-32b-arliai-rpr-v1:free",
"cognitivecomputations/dolphin-mixtral-8x22b",
"cognitivecomputations/dolphin3.0-mistral-24b:free",
"cognitivecomputations/dolphin3.0-r1-mistral-24b:free",
"cohere/command",
"cohere/command-a",
"cohere/command-r",
"cohere/command-r-03-2024",
"cohere/command-r-08-2024",
"cohere/command-r-plus",
"cohere/command-r-plus-04-2024",
"cohere/command-r-plus-08-2024",
"cohere/command-r7b-12-2024",
"deepseek/deepseek-chat",
"deepseek/deepseek-chat-v3-0324",
"deepseek/deepseek-chat-v3-0324:free",
"deepseek/deepseek-chat:free",
"deepseek/deepseek-prover-v2",
"deepseek/deepseek-prover-v2:free",
"deepseek/deepseek-r1",
"deepseek/deepseek-r1-0528",
"deepseek/deepseek-r1-0528-qwen3-8b",
"deepseek/deepseek-r1-0528-qwen3-8b:free",
"deepseek/deepseek-r1-0528:free",
"deepseek/deepseek-r1-distill-llama-70b",
"deepseek/deepseek-r1-distill-llama-70b:free",
"deepseek/deepseek-r1-distill-llama-8b",
"deepseek/deepseek-r1-distill-qwen-1.5b",
"deepseek/deepseek-r1-distill-qwen-14b",
"deepseek/deepseek-r1-distill-qwen-14b:free",
"deepseek/deepseek-r1-distill-qwen-32b",
"deepseek/deepseek-r1-distill-qwen-32b:free",
"deepseek/deepseek-r1-distill-qwen-7b",
"deepseek/deepseek-r1-zero:free",
"deepseek/deepseek-r1:free",
"deepseek/deepseek-v3-base:free",
"eleutherai/llemma_7b",
"eva-unit-01/eva-llama-3.33-70b",
"eva-unit-01/eva-qwen-2.5-32b",
"eva-unit-01/eva-qwen-2.5-72b",
"featherless/qwerky-72b:free",
"google/gemini-2.0-flash-001",
"google/gemini-2.0-flash-exp:free",
"google/gemini-2.0-flash-lite-001",
"google/gemini-2.5-flash-preview",
"google/gemini-2.5-flash-preview-05-20",
"google/gemini-2.5-flash-preview-05-20:thinking",
"google/gemini-2.5-flash-preview:thinking",
"google/gemini-2.5-pro-exp-03-25",
"google/gemini-2.5-pro-preview",
"google/gemini-2.5-pro-preview-05-06",
"google/gemini-flash-1.5",
"google/gemini-flash-1.5-8b",
"google/gemini-pro-1.5",
"google/gemma-2-27b-it",
"google/gemma-2-9b-it",
"google/gemma-2-9b-it:free",
"google/gemma-3-12b-it",
"google/gemma-3-12b-it:free",
"google/gemma-3-1b-it:free",
"google/gemma-3-27b-it",
"google/gemma-3-27b-it:free",
"google/gemma-3-4b-it",
"google/gemma-3-4b-it:free",
"google/gemma-3n-e4b-it:free",
"gryphe/mythomax-l2-13b",
"inception/mercury-coder-small-beta",
"infermatic/mn-inferor-12b",
"inflection/inflection-3-pi",
"inflection/inflection-3-productivity",
"liquid/lfm-3b",
"liquid/lfm-40b",
"liquid/lfm-7b",
"mancer/weaver",
"meta-llama/llama-2-70b-chat",
"meta-llama/llama-3-70b-instruct",
"meta-llama/llama-3-8b-instruct",
"meta-llama/llama-3.1-405b",
"meta-llama/llama-3.1-405b-instruct",
"meta-llama/llama-3.1-405b:free",
"meta-llama/llama-3.1-70b-instruct",
"meta-llama/llama-3.1-8b-instruct",
"meta-llama/llama-3.1-8b-instruct:free",
"meta-llama/llama-3.2-11b-vision-instruct",
"meta-llama/llama-3.2-11b-vision-instruct:free",
"meta-llama/llama-3.2-1b-instruct",
"meta-llama/llama-3.2-1b-instruct:free",
"meta-llama/llama-3.2-3b-instruct",
"meta-llama/llama-3.2-3b-instruct:free",
"meta-llama/llama-3.2-90b-vision-instruct",
"meta-llama/llama-3.3-70b-instruct",
"meta-llama/llama-3.3-70b-instruct:free",
"meta-llama/llama-3.3-8b-instruct:free",
"meta-llama/llama-4-maverick",
"meta-llama/llama-4-maverick:free",
"meta-llama/llama-4-scout",
"meta-llama/llama-4-scout:free",
"meta-llama/llama-guard-2-8b",
"meta-llama/llama-guard-3-8b",
"meta-llama/llama-guard-4-12b",
"microsoft/mai-ds-r1:free",
"microsoft/phi-3-medium-128k-instruct",
"microsoft/phi-3-mini-128k-instruct",
"microsoft/phi-3.5-mini-128k-instruct",
"microsoft/phi-4",
"microsoft/phi-4-multimodal-instruct",
"microsoft/phi-4-reasoning-plus",
"microsoft/phi-4-reasoning-plus:free",
"microsoft/phi-4-reasoning:free",
"microsoft/wizardlm-2-8x22b",
"minimax/minimax-01",
"mistralai/codestral-2501",
"mistralai/devstral-small",
"mistralai/devstral-small:free",
"mistralai/magistral-medium-2506",
"mistralai/magistral-medium-2506:thinking",
"mistralai/magistral-small-2506",
"mistralai/ministral-3b",
"mistralai/ministral-8b",
"mistralai/mistral-7b-instruct",
"mistralai/mistral-7b-instruct-v0.1",
"mistralai/mistral-7b-instruct-v0.2",
"mistralai/mistral-7b-instruct-v0.3",
"mistralai/mistral-7b-instruct:free",
"mistralai/mistral-large",
"mistralai/mistral-large-2407",
"mistralai/mistral-large-2411",
"mistralai/mistral-medium",
"mistralai/mistral-medium-3",
"mistralai/mistral-nemo",
"mistralai/mistral-nemo:free",
"mistralai/mistral-saba",
"mistralai/mistral-small",
"mistralai/mistral-small-24b-instruct-2501",
"mistralai/mistral-small-24b-instruct-2501:free",
"mistralai/mistral-small-3.1-24b-instruct",
"mistralai/mistral-small-3.1-24b-instruct:free",
"mistralai/mistral-tiny",
"mistralai/mixtral-8x22b-instruct",
"mistralai/mixtral-8x7b-instruct",
"mistralai/pixtral-12b",
"mistralai/pixtral-large-2411",
"moonshotai/kimi-vl-a3b-thinking:free",
"moonshotai/moonlight-16b-a3b-instruct:free",
"neversleep/llama-3-lumimaid-70b",
"neversleep/llama-3-lumimaid-8b",
"neversleep/llama-3.1-lumimaid-70b",
"neversleep/llama-3.1-lumimaid-8b",
"neversleep/noromaid-20b",
"nothingiisreal/mn-celeste-12b",
"nousresearch/deephermes-3-llama-3-8b-preview:free",
"nousresearch/deephermes-3-mistral-24b-preview:free",
"nousresearch/hermes-2-pro-llama-3-8b",
"nousresearch/hermes-3-llama-3.1-405b",
"nousresearch/hermes-3-llama-3.1-70b",
"nousresearch/nous-hermes-2-mixtral-8x7b-dpo",
"nvidia/llama-3.1-nemotron-70b-instruct",
"nvidia/llama-3.1-nemotron-ultra-253b-v1",
"nvidia/llama-3.1-nemotron-ultra-253b-v1:free",
"nvidia/llama-3.3-nemotron-super-49b-v1",
"nvidia/llama-3.3-nemotron-super-49b-v1:free",
"open-r1/olympiccoder-32b:free",
"openai/chatgpt-4o-latest",
"openai/codex-mini",
"openai/gpt-3.5-turbo",
"openai/gpt-3.5-turbo-0125",
"openai/gpt-3.5-turbo-0613",
"openai/gpt-3.5-turbo-1106",
"openai/gpt-3.5-turbo-16k",
"openai/gpt-3.5-turbo-instruct",
"openai/gpt-4",
"openai/gpt-4-0314",
"openai/gpt-4-1106-preview",
"openai/gpt-4-turbo",
"openai/gpt-4-turbo-preview",
"openai/gpt-4.1",
"openai/gpt-4.1-mini",
"openai/gpt-4.1-nano",
"openai/gpt-4.5-preview",
"openai/gpt-4o",
"openai/gpt-4o-2024-05-13",
"openai/gpt-4o-2024-08-06",
"openai/gpt-4o-2024-11-20",
"openai/gpt-4o-mini-2024-07-18",
"openai/gpt-4o-mini-search-preview",
"openai/gpt-4o-search-preview",
"openai/gpt-4o:extended",
"openai/o1",
"openai/o1-mini",
"openai/o1-mini-2024-09-12",
"openai/o1-preview",
"openai/o1-preview-2024-09-12",
"openai/o1-pro",
"openai/o3",
"openai/o3-mini",
"openai/o3-mini-high",
"openai/o3-pro",
"openai/o4-mini",
"openai/o4-mini-high",
"opengvlab/internvl3-14b:free",
"opengvlab/internvl3-2b:free",
"openrouter/auto",
"perplexity/llama-3.1-sonar-large-128k-online",
"perplexity/llama-3.1-sonar-small-128k-online",
"perplexity/r1-1776",
"perplexity/sonar",
"perplexity/sonar-deep-research",
"perplexity/sonar-pro",
"perplexity/sonar-reasoning",
"perplexity/sonar-reasoning-pro",
"pygmalionai/mythalion-13b",
"qwen/qwen-2-72b-instruct",
"qwen/qwen-2.5-72b-instruct",
"qwen/qwen-2.5-72b-instruct:free",
"qwen/qwen-2.5-7b-instruct",
"qwen/qwen-2.5-7b-instruct:free",
"qwen/qwen-2.5-coder-32b-instruct",
"qwen/qwen-2.5-coder-32b-instruct:free",
"qwen/qwen-2.5-vl-7b-instruct",
"qwen/qwen-2.5-vl-7b-instruct:free",
"qwen/qwen-max",
"qwen/qwen-plus",
"qwen/qwen-turbo",
"qwen/qwen-vl-max",
"qwen/qwen-vl-plus",
"qwen/qwen2.5-vl-32b-instruct",
"qwen/qwen2.5-vl-32b-instruct:free",
"qwen/qwen2.5-vl-3b-instruct:free",
"qwen/qwen2.5-vl-72b-instruct",
"qwen/qwen2.5-vl-72b-instruct:free",
"qwen/qwen3-14b",
"qwen/qwen3-14b:free",
"qwen/qwen3-235b-a22b",
"qwen/qwen3-235b-a22b:free",
"qwen/qwen3-30b-a3b",
"qwen/qwen3-30b-a3b:free",
"qwen/qwen3-32b",
"qwen/qwen3-32b:free",
"qwen/qwen3-8b",
"qwen/qwen3-8b:free",
"qwen/qwq-32b",
"qwen/qwq-32b-preview",
"qwen/qwq-32b:free",
"raifle/sorcererlm-8x22b",
"rekaai/reka-flash-3:free",
"sao10k/fimbulvetr-11b-v2",
"sao10k/l3-euryale-70b",
"sao10k/l3-lunaris-8b",
"sao10k/l3.1-euryale-70b",
"sao10k/l3.3-euryale-70b",
"sarvamai/sarvam-m:free",
"scb10x/llama3.1-typhoon2-70b-instruct",
"sentientagi/dobby-mini-unhinged-plus-llama-3.1-8b",
"shisa-ai/shisa-v2-llama3.3-70b:free",
"sophosympatheia/midnight-rose-70b",
"thedrummer/anubis-pro-105b-v1",
"thedrummer/rocinante-12b",
"thedrummer/skyfall-36b-v2",
"thedrummer/unslopnemo-12b",
"thedrummer/valkyrie-49b-v1",
"thudm/glm-4-32b",
"thudm/glm-4-32b:free",
"thudm/glm-z1-32b",
"thudm/glm-z1-32b:free",
"thudm/glm-z1-rumination-32b",
"tngtech/deepseek-r1t-chimera:free",
"undi95/remm-slerp-l2-13b",
"undi95/toppy-m-7b",
"x-ai/grok-2-1212",
"x-ai/grok-2-vision-1212",
"x-ai/grok-3-beta",
"x-ai/grok-3-mini-beta",
"x-ai/grok-beta",
"x-ai/grok-vision-beta"
]
};
var getDefaultModel = (provider) => {
switch (provider) {
@@ -71002,6 +71333,8 @@ var getDefaultModel = (provider) => {
return MODEL_LIST.mistral[0];
case "deepseek":
return MODEL_LIST.deepseek[0];
case "openrouter":
return MODEL_LIST.openrouter[0];
default:
return MODEL_LIST.openai[0];
}
@@ -71155,7 +71488,8 @@ var configValidators = {
"test",
"flowise",
"groq",
"deepseek"
"deepseek",
"openrouter"
].includes(value) || value.startsWith("ollama"),
`${value} is not supported yet, use 'ollama', 'mlx', 'anthropic', 'azure', 'gemini', 'flowise', 'mistral', 'deepseek' or 'openai' (default)`
);
@@ -71200,6 +71534,7 @@ var OCO_AI_PROVIDER_ENUM = /* @__PURE__ */ ((OCO_AI_PROVIDER_ENUM2) => {
OCO_AI_PROVIDER_ENUM2["MISTRAL"] = "mistral";
OCO_AI_PROVIDER_ENUM2["MLX"] = "mlx";
OCO_AI_PROVIDER_ENUM2["DEEPSEEK"] = "deepseek";
OCO_AI_PROVIDER_ENUM2["OPENROUTER"] = "openrouter";
return OCO_AI_PROVIDER_ENUM2;
})(OCO_AI_PROVIDER_ENUM || {});
var defaultConfigPath = (0, import_path.join)((0, import_os.homedir)(), ".opencommit");
@@ -71487,7 +71822,11 @@ ${key}:`));
}
}
}
console.log(source_default.yellow('\nUse "oco config describe [PARAMETER]" to see accepted values and more details for a specific config parameter.'));
console.log(
source_default.yellow(
'\nUse "oco config describe [PARAMETER]" to see accepted values and more details for a specific config parameter.'
)
);
}
var configCommand = G2(
{
@@ -86795,6 +87134,40 @@ var DeepseekEngine = class extends OpenAiEngine {
}
};
// src/engine/openrouter.ts
var OpenRouterEngine = class {
constructor(config6) {
this.config = config6;
this.generateCommitMessage = async (messages) => {
try {
const response = await this.client.post("", {
model: this.config.model,
messages
});
const message = response.data.choices[0].message;
let content = message?.content;
return removeContentTags(content, "think");
} catch (error) {
const err = error;
if (axios_default.isAxiosError(error) && error.response?.status === 401) {
const openRouterError = error.response.data.error;
if (openRouterError) throw new Error(openRouterError.message);
}
throw err;
}
};
this.client = axios_default.create({
baseURL: "https://openrouter.ai/api/v1/chat/completions",
headers: {
Authorization: `Bearer ${config6.apiKey}`,
"HTTP-Referer": "https://github.com/di-sukharev/opencommit",
"X-Title": "OpenCommit",
"Content-Type": "application/json"
}
});
}
};
// src/utils/engine.ts
function parseCustomHeaders(headers) {
let parsedHeaders = {};
@@ -86808,7 +87181,9 @@ function parseCustomHeaders(headers) {
parsedHeaders = JSON.parse(headers);
}
} catch (error) {
console.warn("Invalid OCO_API_CUSTOM_HEADERS format, ignoring custom headers");
console.warn(
"Invalid OCO_API_CUSTOM_HEADERS format, ignoring custom headers"
);
}
return parsedHeaders;
}
@@ -86845,6 +87220,8 @@ function getEngine() {
return new MLXEngine(DEFAULT_CONFIG2);
case "deepseek" /* DEEPSEEK */:
return new DeepseekEngine(DEFAULT_CONFIG2);
case "openrouter" /* OPENROUTER */:
return new OpenRouterEngine(DEFAULT_CONFIG2);
default:
return new OpenAiEngine(DEFAULT_CONFIG2);
}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "opencommit",
"version": "3.2.8",
"version": "3.2.10",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "opencommit",
"version": "3.2.8",
"version": "3.2.10",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.10.0",

View File

@@ -1,6 +1,6 @@
{
"name": "opencommit",
"version": "3.2.8",
"version": "3.2.10",
"description": "Auto-generate impressive commits in 1 second. Killing lame commits with AI 🤯🔫",
"keywords": [
"git",
@@ -51,6 +51,7 @@
"deploy:patch": "npm version patch && npm run deploy:build",
"lint": "eslint src --ext ts && tsc --noEmit",
"format": "prettier --write src",
"format:check": "prettier --check src",
"test": "node --no-warnings --experimental-vm-modules $( [ -f ./node_modules/.bin/jest ] && echo ./node_modules/.bin/jest || which jest ) test/unit",
"test:all": "npm run test:unit:docker && npm run test:e2e:docker",
"test:docker-build": "docker build -t oco-test -f test/Dockerfile .",

View File

@@ -1,4 +1,5 @@
import {
text,
confirm,
intro,
isCancel,
@@ -85,15 +86,29 @@ ${commitMessage}
${chalk.grey('——————————————————')}`
);
const isCommitConfirmedByUser =
skipCommitConfirmation ||
(await confirm({
message: 'Confirm the commit message?'
}));
const userAction = skipCommitConfirmation
? 'Yes'
: await select({
message: 'Confirm the commit message?',
options: [
{ value: 'Yes', label: 'Yes' },
{ value: 'No', label: 'No' },
{ value: 'Edit', label: 'Edit' }
]
});
if (isCancel(isCommitConfirmedByUser)) process.exit(1);
if (isCancel(userAction)) process.exit(1);
if (isCommitConfirmedByUser) {
if (userAction === 'Edit') {
const textResponse = await text({
message: 'Please edit the commit message: (press Enter to continue)',
initialValue: commitMessage
});
commitMessage = textResponse.toString();
}
if (userAction === 'Yes' || userAction === 'Edit') {
const committingChangesSpinner = spinner();
committingChangesSpinner.start('Committing the changes');
const { stdout } = await execa('git', [

View File

@@ -27,7 +27,8 @@ export enum CONFIG_KEYS {
OCO_API_URL = 'OCO_API_URL',
OCO_API_CUSTOM_HEADERS = 'OCO_API_CUSTOM_HEADERS',
OCO_OMIT_SCOPE = 'OCO_OMIT_SCOPE',
OCO_GITPUSH = 'OCO_GITPUSH' // todo: deprecate
OCO_GITPUSH = 'OCO_GITPUSH', // todo: deprecate
OCO_HOOK_AUTO_UNCOMMENT = 'OCO_HOOK_AUTO_UNCOMMENT'
}
export enum CONFIG_MODES {
@@ -132,7 +133,443 @@ export const MODEL_LIST = {
'mistral-moderation-2411',
'mistral-moderation-latest'
],
deepseek: ['deepseek-chat', 'deepseek-reasoner']
deepseek: ['deepseek-chat', 'deepseek-reasoner'],
// AI/ML API available chat-completion models
// https://api.aimlapi.com/v1/models
aimlapi: [
'openai/gpt-4o',
'gpt-4o-2024-08-06',
'gpt-4o-2024-05-13',
'gpt-4o-mini',
'gpt-4o-mini-2024-07-18',
'chatgpt-4o-latest',
'gpt-4-turbo',
'gpt-4-turbo-2024-04-09',
'gpt-4',
'gpt-4-0125-preview',
'gpt-4-1106-preview',
'gpt-3.5-turbo',
'gpt-3.5-turbo-0125',
'gpt-3.5-turbo-1106',
'o1-preview',
'o1-preview-2024-09-12',
'o1-mini',
'o1-mini-2024-09-12',
'o3-mini',
'gpt-4o-audio-preview',
'gpt-4o-mini-audio-preview',
'gpt-4o-search-preview',
'gpt-4o-mini-search-preview',
'openai/gpt-4.1-2025-04-14',
'openai/gpt-4.1-mini-2025-04-14',
'openai/gpt-4.1-nano-2025-04-14',
'openai/o4-mini-2025-04-16',
'openai/o3-2025-04-16',
'o1',
'openai/o3-pro',
'meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo',
'google/gemma-2-27b-it',
'meta-llama/Llama-Vision-Free',
'Qwen/Qwen2-72B-Instruct',
'mistralai/Mixtral-8x7B-Instruct-v0.1',
'nvidia/Llama-3.1-Nemotron-70B-Instruct-HF',
'NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO',
'meta-llama/Llama-3.3-70B-Instruct-Turbo',
'meta-llama/Llama-3.2-3B-Instruct-Turbo',
'meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo',
'meta-llama/Llama-Guard-3-11B-Vision-Turbo',
'Qwen/Qwen2.5-7B-Instruct-Turbo',
'Qwen/Qwen2.5-Coder-32B-Instruct',
'meta-llama/Meta-Llama-3-8B-Instruct-Lite',
'meta-llama/Llama-3-8b-chat-hf',
'meta-llama/Llama-3-70b-chat-hf',
'Qwen/Qwen2.5-72B-Instruct-Turbo',
'Qwen/QwQ-32B',
'meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo',
'meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo',
'meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo',
'mistralai/Mistral-7B-Instruct-v0.2',
'meta-llama/LlamaGuard-2-8b',
'mistralai/Mistral-7B-Instruct-v0.1',
'mistralai/Mistral-7B-Instruct-v0.3',
'meta-llama/Meta-Llama-Guard-3-8B',
'meta-llama/llama-4-scout',
'meta-llama/llama-4-maverick',
'Qwen/Qwen3-235B-A22B-fp8-tput',
'claude-3-opus-20240229',
'claude-3-haiku-20240307',
'claude-3-5-sonnet-20240620',
'claude-3-5-sonnet-20241022',
'claude-3-5-haiku-20241022',
'claude-3-7-sonnet-20250219',
'claude-sonnet-4-20250514',
'claude-opus-4-20250514',
'google/gemini-2.0-flash-exp',
'google/gemini-2.0-flash',
'google/gemini-2.5-pro',
'google/gemini-2.5-flash',
'deepseek-chat',
'deepseek-reasoner',
'qwen-max',
'qwen-plus',
'qwen-turbo',
'qwen-max-2025-01-25',
'mistralai/mistral-tiny',
'mistralai/mistral-nemo',
'anthracite-org/magnum-v4-72b',
'nvidia/llama-3.1-nemotron-70b-instruct',
'cohere/command-r-plus',
'mistralai/codestral-2501',
'google/gemma-3-4b-it',
'google/gemma-3-12b-it',
'google/gemma-3-27b-it',
'google/gemini-2.5-flash-lite-preview',
'deepseek/deepseek-prover-v2',
'google/gemma-3n-e4b-it',
'cohere/command-a',
'MiniMax-Text-01',
'abab6.5s-chat',
'minimax/m1',
'bagoodex/bagoodex-search-v1',
'moonshot/kimi-k2-preview',
'perplexity/sonar',
'perplexity/sonar-pro',
'x-ai/grok-4-07-09',
'x-ai/grok-3-beta',
'x-ai/grok-3-mini-beta'
],
// OpenRouter available models
// input_modalities: 'text'
// output_modalities: 'text'
// https://openrouter.ai/api/v1/models
openrouter: [
'openai/gpt-4o-mini', // used by default
'01-ai/yi-large',
'aetherwiing/mn-starcannon-12b',
'agentica-org/deepcoder-14b-preview:free',
'ai21/jamba-1.6-large',
'ai21/jamba-1.6-mini',
'aion-labs/aion-1.0',
'aion-labs/aion-1.0-mini',
'aion-labs/aion-rp-llama-3.1-8b',
'alfredpros/codellama-7b-instruct-solidity',
'all-hands/openhands-lm-32b-v0.1',
'alpindale/goliath-120b',
'alpindale/magnum-72b',
'amazon/nova-lite-v1',
'amazon/nova-micro-v1',
'amazon/nova-pro-v1',
'anthracite-org/magnum-v2-72b',
'anthracite-org/magnum-v4-72b',
'anthropic/claude-2',
'anthropic/claude-2.0',
'anthropic/claude-2.0:beta',
'anthropic/claude-2.1',
'anthropic/claude-2.1:beta',
'anthropic/claude-2:beta',
'anthropic/claude-3-haiku',
'anthropic/claude-3-haiku:beta',
'anthropic/claude-3-opus',
'anthropic/claude-3-opus:beta',
'anthropic/claude-3-sonnet',
'anthropic/claude-3-sonnet:beta',
'anthropic/claude-3.5-haiku',
'anthropic/claude-3.5-haiku-20241022',
'anthropic/claude-3.5-haiku-20241022:beta',
'anthropic/claude-3.5-haiku:beta',
'anthropic/claude-3.5-sonnet',
'anthropic/claude-3.5-sonnet-20240620',
'anthropic/claude-3.5-sonnet-20240620:beta',
'anthropic/claude-3.5-sonnet:beta',
'anthropic/claude-3.7-sonnet',
'anthropic/claude-3.7-sonnet:beta',
'anthropic/claude-3.7-sonnet:thinking',
'anthropic/claude-opus-4',
'anthropic/claude-sonnet-4',
'arcee-ai/arcee-blitz',
'arcee-ai/caller-large',
'arcee-ai/coder-large',
'arcee-ai/maestro-reasoning',
'arcee-ai/spotlight',
'arcee-ai/virtuoso-large',
'arcee-ai/virtuoso-medium-v2',
'arliai/qwq-32b-arliai-rpr-v1:free',
'cognitivecomputations/dolphin-mixtral-8x22b',
'cognitivecomputations/dolphin3.0-mistral-24b:free',
'cognitivecomputations/dolphin3.0-r1-mistral-24b:free',
'cohere/command',
'cohere/command-a',
'cohere/command-r',
'cohere/command-r-03-2024',
'cohere/command-r-08-2024',
'cohere/command-r-plus',
'cohere/command-r-plus-04-2024',
'cohere/command-r-plus-08-2024',
'cohere/command-r7b-12-2024',
'deepseek/deepseek-chat',
'deepseek/deepseek-chat-v3-0324',
'deepseek/deepseek-chat-v3-0324:free',
'deepseek/deepseek-chat:free',
'deepseek/deepseek-prover-v2',
'deepseek/deepseek-prover-v2:free',
'deepseek/deepseek-r1',
'deepseek/deepseek-r1-0528',
'deepseek/deepseek-r1-0528-qwen3-8b',
'deepseek/deepseek-r1-0528-qwen3-8b:free',
'deepseek/deepseek-r1-0528:free',
'deepseek/deepseek-r1-distill-llama-70b',
'deepseek/deepseek-r1-distill-llama-70b:free',
'deepseek/deepseek-r1-distill-llama-8b',
'deepseek/deepseek-r1-distill-qwen-1.5b',
'deepseek/deepseek-r1-distill-qwen-14b',
'deepseek/deepseek-r1-distill-qwen-14b:free',
'deepseek/deepseek-r1-distill-qwen-32b',
'deepseek/deepseek-r1-distill-qwen-32b:free',
'deepseek/deepseek-r1-distill-qwen-7b',
'deepseek/deepseek-r1-zero:free',
'deepseek/deepseek-r1:free',
'deepseek/deepseek-v3-base:free',
'eleutherai/llemma_7b',
'eva-unit-01/eva-llama-3.33-70b',
'eva-unit-01/eva-qwen-2.5-32b',
'eva-unit-01/eva-qwen-2.5-72b',
'featherless/qwerky-72b:free',
'google/gemini-2.0-flash-001',
'google/gemini-2.0-flash-exp:free',
'google/gemini-2.0-flash-lite-001',
'google/gemini-2.5-flash-preview',
'google/gemini-2.5-flash-preview-05-20',
'google/gemini-2.5-flash-preview-05-20:thinking',
'google/gemini-2.5-flash-preview:thinking',
'google/gemini-2.5-pro-exp-03-25',
'google/gemini-2.5-pro-preview',
'google/gemini-2.5-pro-preview-05-06',
'google/gemini-flash-1.5',
'google/gemini-flash-1.5-8b',
'google/gemini-pro-1.5',
'google/gemma-2-27b-it',
'google/gemma-2-9b-it',
'google/gemma-2-9b-it:free',
'google/gemma-3-12b-it',
'google/gemma-3-12b-it:free',
'google/gemma-3-1b-it:free',
'google/gemma-3-27b-it',
'google/gemma-3-27b-it:free',
'google/gemma-3-4b-it',
'google/gemma-3-4b-it:free',
'google/gemma-3n-e4b-it:free',
'gryphe/mythomax-l2-13b',
'inception/mercury-coder-small-beta',
'infermatic/mn-inferor-12b',
'inflection/inflection-3-pi',
'inflection/inflection-3-productivity',
'liquid/lfm-3b',
'liquid/lfm-40b',
'liquid/lfm-7b',
'mancer/weaver',
'meta-llama/llama-2-70b-chat',
'meta-llama/llama-3-70b-instruct',
'meta-llama/llama-3-8b-instruct',
'meta-llama/llama-3.1-405b',
'meta-llama/llama-3.1-405b-instruct',
'meta-llama/llama-3.1-405b:free',
'meta-llama/llama-3.1-70b-instruct',
'meta-llama/llama-3.1-8b-instruct',
'meta-llama/llama-3.1-8b-instruct:free',
'meta-llama/llama-3.2-11b-vision-instruct',
'meta-llama/llama-3.2-11b-vision-instruct:free',
'meta-llama/llama-3.2-1b-instruct',
'meta-llama/llama-3.2-1b-instruct:free',
'meta-llama/llama-3.2-3b-instruct',
'meta-llama/llama-3.2-3b-instruct:free',
'meta-llama/llama-3.2-90b-vision-instruct',
'meta-llama/llama-3.3-70b-instruct',
'meta-llama/llama-3.3-70b-instruct:free',
'meta-llama/llama-3.3-8b-instruct:free',
'meta-llama/llama-4-maverick',
'meta-llama/llama-4-maverick:free',
'meta-llama/llama-4-scout',
'meta-llama/llama-4-scout:free',
'meta-llama/llama-guard-2-8b',
'meta-llama/llama-guard-3-8b',
'meta-llama/llama-guard-4-12b',
'microsoft/mai-ds-r1:free',
'microsoft/phi-3-medium-128k-instruct',
'microsoft/phi-3-mini-128k-instruct',
'microsoft/phi-3.5-mini-128k-instruct',
'microsoft/phi-4',
'microsoft/phi-4-multimodal-instruct',
'microsoft/phi-4-reasoning-plus',
'microsoft/phi-4-reasoning-plus:free',
'microsoft/phi-4-reasoning:free',
'microsoft/wizardlm-2-8x22b',
'minimax/minimax-01',
'mistralai/codestral-2501',
'mistralai/devstral-small',
'mistralai/devstral-small:free',
'mistralai/magistral-medium-2506',
'mistralai/magistral-medium-2506:thinking',
'mistralai/magistral-small-2506',
'mistralai/ministral-3b',
'mistralai/ministral-8b',
'mistralai/mistral-7b-instruct',
'mistralai/mistral-7b-instruct-v0.1',
'mistralai/mistral-7b-instruct-v0.2',
'mistralai/mistral-7b-instruct-v0.3',
'mistralai/mistral-7b-instruct:free',
'mistralai/mistral-large',
'mistralai/mistral-large-2407',
'mistralai/mistral-large-2411',
'mistralai/mistral-medium',
'mistralai/mistral-medium-3',
'mistralai/mistral-nemo',
'mistralai/mistral-nemo:free',
'mistralai/mistral-saba',
'mistralai/mistral-small',
'mistralai/mistral-small-24b-instruct-2501',
'mistralai/mistral-small-24b-instruct-2501:free',
'mistralai/mistral-small-3.1-24b-instruct',
'mistralai/mistral-small-3.1-24b-instruct:free',
'mistralai/mistral-tiny',
'mistralai/mixtral-8x22b-instruct',
'mistralai/mixtral-8x7b-instruct',
'mistralai/pixtral-12b',
'mistralai/pixtral-large-2411',
'moonshotai/kimi-vl-a3b-thinking:free',
'moonshotai/moonlight-16b-a3b-instruct:free',
'neversleep/llama-3-lumimaid-70b',
'neversleep/llama-3-lumimaid-8b',
'neversleep/llama-3.1-lumimaid-70b',
'neversleep/llama-3.1-lumimaid-8b',
'neversleep/noromaid-20b',
'nothingiisreal/mn-celeste-12b',
'nousresearch/deephermes-3-llama-3-8b-preview:free',
'nousresearch/deephermes-3-mistral-24b-preview:free',
'nousresearch/hermes-2-pro-llama-3-8b',
'nousresearch/hermes-3-llama-3.1-405b',
'nousresearch/hermes-3-llama-3.1-70b',
'nousresearch/nous-hermes-2-mixtral-8x7b-dpo',
'nvidia/llama-3.1-nemotron-70b-instruct',
'nvidia/llama-3.1-nemotron-ultra-253b-v1',
'nvidia/llama-3.1-nemotron-ultra-253b-v1:free',
'nvidia/llama-3.3-nemotron-super-49b-v1',
'nvidia/llama-3.3-nemotron-super-49b-v1:free',
'open-r1/olympiccoder-32b:free',
'openai/chatgpt-4o-latest',
'openai/codex-mini',
'openai/gpt-3.5-turbo',
'openai/gpt-3.5-turbo-0125',
'openai/gpt-3.5-turbo-0613',
'openai/gpt-3.5-turbo-1106',
'openai/gpt-3.5-turbo-16k',
'openai/gpt-3.5-turbo-instruct',
'openai/gpt-4',
'openai/gpt-4-0314',
'openai/gpt-4-1106-preview',
'openai/gpt-4-turbo',
'openai/gpt-4-turbo-preview',
'openai/gpt-4.1',
'openai/gpt-4.1-mini',
'openai/gpt-4.1-nano',
'openai/gpt-4.5-preview',
'openai/gpt-4o',
'openai/gpt-4o-2024-05-13',
'openai/gpt-4o-2024-08-06',
'openai/gpt-4o-2024-11-20',
'openai/gpt-4o-mini-2024-07-18',
'openai/gpt-4o-mini-search-preview',
'openai/gpt-4o-search-preview',
'openai/gpt-4o:extended',
'openai/o1',
'openai/o1-mini',
'openai/o1-mini-2024-09-12',
'openai/o1-preview',
'openai/o1-preview-2024-09-12',
'openai/o1-pro',
'openai/o3',
'openai/o3-mini',
'openai/o3-mini-high',
'openai/o3-pro',
'openai/o4-mini',
'openai/o4-mini-high',
'opengvlab/internvl3-14b:free',
'opengvlab/internvl3-2b:free',
'openrouter/auto',
'perplexity/llama-3.1-sonar-large-128k-online',
'perplexity/llama-3.1-sonar-small-128k-online',
'perplexity/r1-1776',
'perplexity/sonar',
'perplexity/sonar-deep-research',
'perplexity/sonar-pro',
'perplexity/sonar-reasoning',
'perplexity/sonar-reasoning-pro',
'pygmalionai/mythalion-13b',
'qwen/qwen-2-72b-instruct',
'qwen/qwen-2.5-72b-instruct',
'qwen/qwen-2.5-72b-instruct:free',
'qwen/qwen-2.5-7b-instruct',
'qwen/qwen-2.5-7b-instruct:free',
'qwen/qwen-2.5-coder-32b-instruct',
'qwen/qwen-2.5-coder-32b-instruct:free',
'qwen/qwen-2.5-vl-7b-instruct',
'qwen/qwen-2.5-vl-7b-instruct:free',
'qwen/qwen-max',
'qwen/qwen-plus',
'qwen/qwen-turbo',
'qwen/qwen-vl-max',
'qwen/qwen-vl-plus',
'qwen/qwen2.5-vl-32b-instruct',
'qwen/qwen2.5-vl-32b-instruct:free',
'qwen/qwen2.5-vl-3b-instruct:free',
'qwen/qwen2.5-vl-72b-instruct',
'qwen/qwen2.5-vl-72b-instruct:free',
'qwen/qwen3-14b',
'qwen/qwen3-14b:free',
'qwen/qwen3-235b-a22b',
'qwen/qwen3-235b-a22b:free',
'qwen/qwen3-30b-a3b',
'qwen/qwen3-30b-a3b:free',
'qwen/qwen3-32b',
'qwen/qwen3-32b:free',
'qwen/qwen3-8b',
'qwen/qwen3-8b:free',
'qwen/qwq-32b',
'qwen/qwq-32b-preview',
'qwen/qwq-32b:free',
'raifle/sorcererlm-8x22b',
'rekaai/reka-flash-3:free',
'sao10k/fimbulvetr-11b-v2',
'sao10k/l3-euryale-70b',
'sao10k/l3-lunaris-8b',
'sao10k/l3.1-euryale-70b',
'sao10k/l3.3-euryale-70b',
'sarvamai/sarvam-m:free',
'scb10x/llama3.1-typhoon2-70b-instruct',
'sentientagi/dobby-mini-unhinged-plus-llama-3.1-8b',
'shisa-ai/shisa-v2-llama3.3-70b:free',
'sophosympatheia/midnight-rose-70b',
'thedrummer/anubis-pro-105b-v1',
'thedrummer/rocinante-12b',
'thedrummer/skyfall-36b-v2',
'thedrummer/unslopnemo-12b',
'thedrummer/valkyrie-49b-v1',
'thudm/glm-4-32b',
'thudm/glm-4-32b:free',
'thudm/glm-z1-32b',
'thudm/glm-z1-32b:free',
'thudm/glm-z1-rumination-32b',
'tngtech/deepseek-r1t-chimera:free',
'undi95/remm-slerp-l2-13b',
'undi95/toppy-m-7b',
'x-ai/grok-2-1212',
'x-ai/grok-2-vision-1212',
'x-ai/grok-3-beta',
'x-ai/grok-3-mini-beta',
'x-ai/grok-beta',
'x-ai/grok-vision-beta'
]
};
const getDefaultModel = (provider: string | undefined): string => {
@@ -151,6 +588,10 @@ const getDefaultModel = (provider: string | undefined): string => {
return MODEL_LIST.mistral[0];
case 'deepseek':
return MODEL_LIST.deepseek[0];
case 'aimlapi':
return MODEL_LIST.aimlapi[0];
case 'openrouter':
return MODEL_LIST.openrouter[0];
default:
return MODEL_LIST.openai[0];
}
@@ -340,9 +781,11 @@ export const configValidators = {
'test',
'flowise',
'groq',
'deepseek'
'deepseek',
'aimlapi',
'openrouter'
].includes(value) || value.startsWith('ollama'),
`${value} is not supported yet, use 'ollama', 'mlx', 'anthropic', 'azure', 'gemini', 'flowise', 'mistral', 'deepseek' or 'openai' (default)`
`${value} is not supported yet, use 'ollama', 'mlx', 'anthropic', 'azure', 'gemini', 'flowise', 'mistral', 'deepseek', 'aimlapi' or 'openai' (default)`
);
return value;
@@ -376,6 +819,14 @@ export const configValidators = {
'Must be true or false'
);
return value;
},
[CONFIG_KEYS.OCO_HOOK_AUTO_UNCOMMENT](value: any) {
validateConfig(
CONFIG_KEYS.OCO_HOOK_AUTO_UNCOMMENT,
typeof value === 'boolean',
'Must be true or false'
);
}
};
@@ -390,7 +841,9 @@ export enum OCO_AI_PROVIDER_ENUM {
GROQ = 'groq',
MISTRAL = 'mistral',
MLX = 'mlx',
DEEPSEEK = 'deepseek'
DEEPSEEK = 'deepseek',
AIMLAPI = 'aimlapi',
OPENROUTER = 'openrouter'
}
export type ConfigType = {
@@ -411,6 +864,7 @@ export type ConfigType = {
[CONFIG_KEYS.OCO_ONE_LINE_COMMIT]: boolean;
[CONFIG_KEYS.OCO_OMIT_SCOPE]: boolean;
[CONFIG_KEYS.OCO_TEST_MOCK_TYPE]: string;
[CONFIG_KEYS.OCO_HOOK_AUTO_UNCOMMENT]: boolean;
};
export const defaultConfigPath = pathJoin(homedir(), '.opencommit');
@@ -458,7 +912,8 @@ export const DEFAULT_CONFIG = {
OCO_TEST_MOCK_TYPE: 'commit-message',
OCO_WHY: false,
OCO_OMIT_SCOPE: false,
OCO_GITPUSH: true // todo: deprecate
OCO_GITPUSH: true, // todo: deprecate
OCO_HOOK_AUTO_UNCOMMENT: false
};
const initGlobalConfig = (configPath: string = defaultConfigPath) => {
@@ -658,7 +1113,8 @@ function getConfigKeyDetails(key) {
};
case CONFIG_KEYS.OCO_DESCRIPTION:
return {
description: 'Postface a message with ~3 sentences description of the changes',
description:
'Postface a message with ~3 sentences description of the changes',
values: ['true', 'false']
};
case CONFIG_KEYS.OCO_EMOJI:
@@ -668,9 +1124,10 @@ function getConfigKeyDetails(key) {
};
case CONFIG_KEYS.OCO_WHY:
return {
description: 'Output a short description of why the changes were done after the commit message (default: false)',
description:
'Output a short description of why the changes were done after the commit message (default: false)',
values: ['true', 'false']
}
};
case CONFIG_KEYS.OCO_OMIT_SCOPE:
return {
description: 'Do not include a scope in the commit message',
@@ -678,7 +1135,8 @@ function getConfigKeyDetails(key) {
};
case CONFIG_KEYS.OCO_GITPUSH:
return {
description: 'Push to git after commit (deprecated). If false, oco will exit after committing',
description:
'Push to git after commit (deprecated). If false, oco will exit after committing',
values: ['true', 'false']
};
case CONFIG_KEYS.OCO_TOKENS_MAX_INPUT:
@@ -698,13 +1156,19 @@ function getConfigKeyDetails(key) {
};
case CONFIG_KEYS.OCO_API_URL:
return {
description: 'Custom API URL - may be used to set proxy path to OpenAI API',
description:
'Custom API URL - may be used to set proxy path to OpenAI API',
values: ["URL string (must start with 'http://' or 'https://')"]
};
case CONFIG_KEYS.OCO_MESSAGE_TEMPLATE_PLACEHOLDER:
return {
description: 'Message template placeholder',
values: ["String (must start with $)"]
values: ['String (must start with $)']
};
case CONFIG_KEYS.OCO_HOOK_AUTO_UNCOMMENT:
return {
description: 'Automatically uncomment the commit message in the hook',
values: ['true', 'false']
};
default:
return {
@@ -728,7 +1192,6 @@ function printConfigKeyHelp(param) {
defaultValue = DEFAULT_CONFIG[param];
}
console.log(chalk.bold(`\n${param}:`));
console.log(chalk.gray(` Description: ${desc}`));
if (defaultValue !== undefined) {
@@ -742,14 +1205,14 @@ function printConfigKeyHelp(param) {
if (Array.isArray(details.values)) {
console.log(chalk.gray(' Accepted values:'));
details.values.forEach(value => {
details.values.forEach((value) => {
console.log(chalk.gray(` - ${value}`));
});
} else {
console.log(chalk.gray(' Accepted values by provider:'));
Object.entries(details.values).forEach(([provider, values]) => {
console.log(chalk.gray(` ${provider}:`));
(values as string[]).forEach(value => {
(values as string[]).forEach((value) => {
console.log(chalk.gray(` - ${value}`));
});
});
@@ -765,7 +1228,7 @@ function printAllConfigHelp() {
if (key in DEFAULT_CONFIG) {
defaultValue = DEFAULT_CONFIG[key];
}
console.log(chalk.bold(`\n${key}:`));
console.log(chalk.gray(` Description: ${details.description}`));
if (defaultValue !== undefined) {
@@ -776,7 +1239,11 @@ function printAllConfigHelp() {
}
}
}
console.log(chalk.yellow('\nUse "oco config describe [PARAMETER]" to see accepted values and more details for a specific config parameter.'));
console.log(
chalk.yellow(
'\nUse "oco config describe [PARAMETER]" to see accepted values and more details for a specific config parameter.'
)
);
}
export const configCommand = command(

View File

@@ -56,10 +56,14 @@ export const prepareCommitMessageHook = async (
const fileContent = await fs.readFile(messageFilePath);
await fs.writeFile(
messageFilePath,
commitMessage + '\n' + fileContent.toString()
);
const messageWithComment = `# ${commitMessage}\n\n# ---------- [OpenCommit] ---------- #\n# Remove the # above to use this generated commit message.\n# To cancel the commit, just close this window without making any changes.\n\n${fileContent.toString()}`;
const messageWithoutComment = `${commitMessage}\n\n${fileContent.toString()}`;
const message = config.OCO_HOOK_AUTO_UNCOMMENT
? messageWithoutComment
: messageWithComment;
await fs.writeFile(messageFilePath, message);
} catch (error) {
outro(`${chalk.red('✖')} ${error}`);
process.exit(1);

47
src/engine/aimlapi.ts Normal file
View File

@@ -0,0 +1,47 @@
import OpenAI from 'openai';
import axios, { AxiosInstance } from 'axios';
import { AiEngine, AiEngineConfig } from './Engine';
interface AimlApiConfig extends AiEngineConfig {}
export class AimlApiEngine implements AiEngine {
client: AxiosInstance;
constructor(public config: AimlApiConfig) {
this.client = axios.create({
baseURL: config.baseURL || 'https://api.aimlapi.com/v1/chat/completions',
headers: {
Authorization: `Bearer ${config.apiKey}`,
'HTTP-Referer': 'https://github.com/di-sukharev/opencommit',
'X-Title': 'opencommit',
'Content-Type': 'application/json',
...config.customHeaders
}
});
}
public generateCommitMessage = async (
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
): Promise<string | null> => {
try {
const response = await this.client.post('', {
model: this.config.model,
messages
});
const message = response.data.choices?.[0]?.message;
return message?.content ?? null;
} catch (error) {
const err = error as Error;
if (
axios.isAxiosError<{ error?: { message: string } }>(error) &&
error.response?.status === 401
) {
const apiError = error.response.data.error;
if (apiError) throw new Error(apiError.message);
}
throw err;
}
};
}

View File

@@ -53,7 +53,7 @@ export class AzureEngine implements AiEngine {
if (message?.content === null) {
return undefined;
}
let content = message?.content;
return removeContentTags(content, 'think');
} catch (error) {

View File

@@ -7,4 +7,4 @@ export class GroqEngine extends OpenAiEngine {
config.baseURL = 'https://api.groq.com/openai/v1';
super(config);
}
}
}

View File

@@ -23,7 +23,10 @@ export class MistralAiEngine implements AiEngine {
if (!config.baseURL) {
this.client = new Mistral({ apiKey: config.apiKey });
} else {
this.client = new Mistral({ apiKey: config.apiKey, serverURL: config.baseURL });
this.client = new Mistral({
apiKey: config.apiKey,
serverURL: config.baseURL
});
}
}
@@ -50,13 +53,12 @@ export class MistralAiEngine implements AiEngine {
const completion = await this.client.chat.complete(params);
if (!completion.choices)
throw Error('No completion choice available.')
if (!completion.choices) throw Error('No completion choice available.');
const message = completion.choices[0].message;
if (!message || !message.content)
throw Error('No completion choice available.')
throw Error('No completion choice available.');
let content = message.content as string;
return removeContentTags(content, 'think');

View File

@@ -6,42 +6,42 @@ import { AiEngine, AiEngineConfig } from './Engine';
interface MLXConfig extends AiEngineConfig {}
export class MLXEngine implements AiEngine {
config: MLXConfig;
client: AxiosInstance;
config: MLXConfig;
client: AxiosInstance;
constructor(config) {
this.config = config;
this.client = axios.create({
url: config.baseURL
? `${config.baseURL}/${config.apiKey}`
: 'http://localhost:8080/v1/chat/completions',
headers: { 'Content-Type': 'application/json' }
});
constructor(config) {
this.config = config;
this.client = axios.create({
url: config.baseURL
? `${config.baseURL}/${config.apiKey}`
: 'http://localhost:8080/v1/chat/completions',
headers: { 'Content-Type': 'application/json' }
});
}
async generateCommitMessage(
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
): Promise<string | undefined> {
const params = {
messages,
temperature: 0,
top_p: 0.1,
repetition_penalty: 1.5,
stream: false
};
try {
const response = await this.client.post(
this.client.getUri(this.config),
params
);
const choices = response.data.choices;
const message = choices[0].message;
let content = message?.content;
return removeContentTags(content, 'think');
} catch (err: any) {
const message = err.response?.data?.error ?? err.message;
throw new Error(`MLX provider error: ${message}`);
}
async generateCommitMessage(
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>):
Promise<string | undefined> {
const params = {
messages,
temperature: 0,
top_p: 0.1,
repetition_penalty: 1.5,
stream: false
};
try {
const response = await this.client.post(
this.client.getUri(this.config),
params
);
const choices = response.data.choices;
const message = choices[0].message;
let content = message?.content;
return removeContentTags(content, 'think');
} catch (err: any) {
const message = err.response?.data?.error ?? err.message;
throw new Error(`MLX provider error: ${message}`);
}
}
}
}

View File

@@ -11,13 +11,13 @@ export class OllamaEngine implements AiEngine {
constructor(config) {
this.config = config;
// Combine base headers with custom headers
const headers = {
const headers = {
'Content-Type': 'application/json',
...config.customHeaders
...config.customHeaders
};
this.client = axios.create({
url: config.baseURL
? `${config.baseURL}/${config.apiKey}`

View File

@@ -18,18 +18,18 @@ export class OpenAiEngine implements AiEngine {
const clientOptions: OpenAI.ClientOptions = {
apiKey: config.apiKey
};
if (config.baseURL) {
clientOptions.baseURL = config.baseURL;
}
if (config.customHeaders) {
const headers = parseCustomHeaders(config.customHeaders);
if (Object.keys(headers).length > 0) {
clientOptions.defaultHeaders = headers;
}
}
this.client = new OpenAI(clientOptions);
}
@@ -54,7 +54,7 @@ export class OpenAiEngine implements AiEngine {
this.config.maxTokensInput - this.config.maxTokensOutput
)
throw new Error(GenerateCommitMessageErrorEnum.tooMuchTokens);
const completion = await this.client.chat.completions.create(params);
const message = completion.choices[0].message;

49
src/engine/openrouter.ts Normal file
View File

@@ -0,0 +1,49 @@
import OpenAI from 'openai';
import { AiEngine, AiEngineConfig } from './Engine';
import axios, { AxiosInstance } from 'axios';
import { removeContentTags } from '../utils/removeContentTags';
interface OpenRouterConfig extends AiEngineConfig {}
export class OpenRouterEngine implements AiEngine {
client: AxiosInstance;
constructor(public config: OpenRouterConfig) {
this.client = axios.create({
baseURL: 'https://openrouter.ai/api/v1/chat/completions',
headers: {
Authorization: `Bearer ${config.apiKey}`,
'HTTP-Referer': 'https://github.com/di-sukharev/opencommit',
'X-Title': 'OpenCommit',
'Content-Type': 'application/json'
}
});
}
public generateCommitMessage = async (
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
): Promise<string | null> => {
try {
const response = await this.client.post('', {
model: this.config.model,
messages
});
const message = response.data.choices[0].message;
let content = message?.content;
return removeContentTags(content, 'think');
} catch (error) {
const err = error as Error;
if (
axios.isAxiosError<{ error?: { message: string } }>(error) &&
error.response?.status === 401
) {
const openRouterError = error.response.data.error;
if (openRouterError) throw new Error(openRouterError.message);
}
throw err;
}
};
}

View File

@@ -14,7 +14,10 @@ const generateCommitMessageChatCompletionPrompt = async (
fullGitMojiSpec: boolean,
context: string
): Promise<Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>> => {
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(fullGitMojiSpec, context);
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(
fullGitMojiSpec,
context
);
const chatContextAsCompletionRequest = [...INIT_MESSAGES_PROMPT];
@@ -38,7 +41,7 @@ const ADJUSTMENT_FACTOR = 20;
export const generateCommitMessageByDiff = async (
diff: string,
fullGitMojiSpec: boolean = false,
context: string = ""
context: string = ''
): Promise<string> => {
try {
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(
@@ -75,7 +78,7 @@ export const generateCommitMessageByDiff = async (
const messages = await generateCommitMessageChatCompletionPrompt(
diff,
fullGitMojiSpec,
context,
context
);
const engine = getEngine();

View File

@@ -1,8 +1,8 @@
{
"localLanguage": "한국어",
"commitFix": "fix(server.ts): 포트 변수를 소문자 port에서 대문자 PORT로 변경",
"commitFeat": "피트(server.ts): process.env.PORT 환경 변수 지원 추가",
"commitFeat": "feat(server.ts): process.env.PORT 환경 변수 지원 추가",
"commitDescription": "포트 변수는 이제 PORT로 이름이 지정되어 상수인 PORT와 일관성 있는 이름 규칙을 따릅니다. 환경 변수 지원을 통해 애플리케이션은 이제 process.env.PORT 환경 변수로 지정된 사용 가능한 모든 포트에서 실행할 수 있으므로 더 유연해졌습니다.",
"commitFixOmitScope": "fix: 포트 변수를 소문자 port에서 대문자 PORT로 변경",
"commitFeatOmitScope": "피트: process.env.PORT 환경 변수 지원 추가"
"commitFeatOmitScope": "feat: process.env.PORT 환경 변수 지원 추가"
}

View File

@@ -36,6 +36,19 @@ export const runMigrations = async () => {
const config = getConfig();
if (config.OCO_AI_PROVIDER === OCO_AI_PROVIDER_ENUM.TEST) return;
// skip unhandled providers in migration00
if (
[
OCO_AI_PROVIDER_ENUM.DEEPSEEK,
OCO_AI_PROVIDER_ENUM.GROQ,
OCO_AI_PROVIDER_ENUM.MISTRAL,
OCO_AI_PROVIDER_ENUM.MLX,
OCO_AI_PROVIDER_ENUM.OPENROUTER
].includes(config.OCO_AI_PROVIDER)
) {
return;
}
const completedMigrations = getCompletedMigrations();
let isMigrated = false;

View File

@@ -56,10 +56,11 @@ const llmReadableRules: {
blankline: (key, applicable) =>
`There should ${applicable} be a blank line at the beginning of the ${key}.`,
caseRule: (key, applicable, value: string | Array<string>) =>
`The ${key} should ${applicable} be in ${Array.isArray(value)
? `one of the following case:
`The ${key} should ${applicable} be in ${
Array.isArray(value)
? `one of the following case:
- ${value.join('\n - ')}.`
: `${value} case.`
: `${value} case.`
}`,
emptyRule: (key, applicable) => `The ${key} should ${applicable} be empty.`,
enumRule: (key, applicable, value: string | Array<string>) =>
@@ -67,17 +68,18 @@ const llmReadableRules: {
- ${Array.isArray(value) ? value.join('\n - ') : value}.`,
enumTypeRule: (key, applicable, value: string | Array<string>, prompt) =>
`The ${key} should ${applicable} be one of the following values:
- ${Array.isArray(value)
- ${
Array.isArray(value)
? value
.map((v) => {
const description = getTypeRuleExtraDescription(v, prompt);
if (description) {
return `${v} (${description})`;
} else return v;
})
.join('\n - ')
.map((v) => {
const description = getTypeRuleExtraDescription(v, prompt);
if (description) {
return `${v} (${description})`;
} else return v;
})
.join('\n - ')
: value
}.`,
}.`,
fullStopRule: (key, applicable, value: string) =>
`The ${key} should ${applicable} end with '${value}'.`,
maxLengthRule: (key, applicable, value: string) =>
@@ -214,16 +216,20 @@ const STRUCTURE_OF_COMMIT = config.OCO_OMIT_SCOPE
const GEN_COMMITLINT_CONSISTENCY_PROMPT = (
prompts: string[]
): OpenAI.Chat.Completions.ChatCompletionMessageParam[] => [
{
role: 'system',
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages for two different changes in a single codebase and output them in the provided JSON format: one for a bug fix and another for a new feature.
{
role: 'system',
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages for two different changes in a single codebase and output them in the provided JSON format: one for a bug fix and another for a new feature.
Here are the specific requirements and conventions that should be strictly followed:
Commit Message Conventions:
- The commit message consists of three parts: Header, Body, and Footer.
- Header:
- Format: ${config.OCO_OMIT_SCOPE ? '`<type>: <subject>`' : '`<type>(<scope>): <subject>`'}
- Format: ${
config.OCO_OMIT_SCOPE
? '`<type>: <subject>`'
: '`<type>(<scope>): <subject>`'
}
- ${prompts.join('\n- ')}
JSON Output Format:
@@ -246,9 +252,9 @@ Additional Details:
- Allowing the server to listen on a port specified through the environment variable is considered a new feature.
Example Git Diff is to follow:`
},
INIT_DIFF_PROMPT
];
},
INIT_DIFF_PROMPT
];
/**
* Prompt to have LLM generate a message using @commitlint rules.
@@ -262,25 +268,30 @@ const INIT_MAIN_PROMPT = (
prompts: string[]
): OpenAI.Chat.Completions.ChatCompletionMessageParam => ({
role: 'system',
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages in the given @commitlint convention and explain WHAT were the changes ${config.OCO_WHY ? 'and WHY the changes were done' : ''
}. I'll send you an output of 'git diff --staged' command, and you convert it into a commit message.
${config.OCO_EMOJI
? 'Use GitMoji convention to preface the commit.'
: 'Do not preface the commit with anything.'
}
${config.OCO_DESCRIPTION
? 'Add a short description of WHY the changes are done after the commit message. Don\'t start it with "This commit", just describe the changes.'
: "Don't add any descriptions to the commit, only commit message."
}
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages in the given @commitlint convention and explain WHAT were the changes ${
config.OCO_WHY ? 'and WHY the changes were done' : ''
}. I'll send you an output of 'git diff --staged' command, and you convert it into a commit message.
${
config.OCO_EMOJI
? 'Use GitMoji convention to preface the commit.'
: 'Do not preface the commit with anything.'
}
${
config.OCO_DESCRIPTION
? 'Add a short description of WHY the changes are done after the commit message. Don\'t start it with "This commit", just describe the changes.'
: "Don't add any descriptions to the commit, only commit message."
}
Use the present tense. Use ${language} to answer.
${config.OCO_ONE_LINE_COMMIT
? 'Craft a concise commit message that encapsulates all changes made, with an emphasis on the primary updates. If the modifications share a common theme or scope, mention it succinctly; otherwise, leave the scope out to maintain focus. The goal is to provide a clear and unified overview of the changes in a one single message, without diverging into a list of commit per file change.'
: ''
}
${config.OCO_OMIT_SCOPE
? 'Do not include a scope in the commit message format. Use the format: <type>: <subject>'
: ''
}
${
config.OCO_ONE_LINE_COMMIT
? 'Craft a concise commit message that encapsulates all changes made, with an emphasis on the primary updates. If the modifications share a common theme or scope, mention it succinctly; otherwise, leave the scope out to maintain focus. The goal is to provide a clear and unified overview of the changes in a one single message, without diverging into a list of commit per file change.'
: ''
}
${
config.OCO_OMIT_SCOPE
? 'Do not include a scope in the commit message format. Use the format: <type>: <subject>'
: ''
}
You will strictly follow the following conventions to generate the content of the commit message:
- ${prompts.join('\n- ')}

View File

@@ -21,7 +21,7 @@ export const getJSONBlock = (input: string): string => {
if (jsonIndex > -1) {
input = input.slice(jsonIndex + 8);
const endJsonIndex = input.search('```');
input = input.slice(0, endJsonIndex);
input = input.slice(0, endJsonIndex);
}
return input;
};

View File

@@ -155,9 +155,9 @@ const INIT_MAIN_PROMPT = (
});
export const INIT_DIFF_PROMPT: OpenAI.Chat.Completions.ChatCompletionMessageParam =
{
role: 'user',
content: `diff --git a/src/server.ts b/src/server.ts
{
role: 'user',
content: `diff --git a/src/server.ts b/src/server.ts
index ad4db42..f3b18a9 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -181,7 +181,7 @@ export const INIT_DIFF_PROMPT: OpenAI.Chat.Completions.ChatCompletionMessagePara
+app.listen(process.env.PORT || PORT, () => {
+ console.log(\`Server listening on port \${PORT}\`);
});`
};
};
const COMMIT_TYPES = {
fix: '🐛',
@@ -193,19 +193,19 @@ const generateCommitString = (
message: string
): string => {
const cleanMessage = removeConventionalCommitWord(message);
return config.OCO_EMOJI
? `${COMMIT_TYPES[type]} ${cleanMessage}`
: message;
return config.OCO_EMOJI ? `${COMMIT_TYPES[type]} ${cleanMessage}` : message;
};
const getConsistencyContent = (translation: ConsistencyPrompt) => {
const fixMessage = config.OCO_OMIT_SCOPE && translation.commitFixOmitScope
? translation.commitFixOmitScope
: translation.commitFix;
const fixMessage =
config.OCO_OMIT_SCOPE && translation.commitFixOmitScope
? translation.commitFixOmitScope
: translation.commitFix;
const featMessage = config.OCO_OMIT_SCOPE && translation.commitFeatOmitScope
? translation.commitFeatOmitScope
: translation.commitFeat;
const featMessage =
config.OCO_OMIT_SCOPE && translation.commitFeatOmitScope
? translation.commitFeatOmitScope
: translation.commitFeat;
const fix = generateCommitString('fix', fixMessage);
const feat = config.OCO_ONE_LINE_COMMIT
@@ -250,7 +250,7 @@ export const getMainCommitPrompt = async (
INIT_DIFF_PROMPT,
INIT_CONSISTENCY_PROMPT(
commitLintConfig.consistency[
translation.localLanguage
translation.localLanguage
] as ConsistencyPrompt
)
];

View File

@@ -11,14 +11,16 @@ import { TestAi, TestMockType } from '../engine/testAi';
import { GroqEngine } from '../engine/groq';
import { MLXEngine } from '../engine/mlx';
import { DeepseekEngine } from '../engine/deepseek';
import { AimlApiEngine } from '../engine/aimlapi';
import { OpenRouterEngine } from '../engine/openrouter';
export function parseCustomHeaders(headers: any): Record<string, string> {
let parsedHeaders = {};
if (!headers) {
return parsedHeaders;
}
try {
if (typeof headers === 'object' && !Array.isArray(headers)) {
parsedHeaders = headers;
@@ -26,9 +28,11 @@ export function parseCustomHeaders(headers: any): Record<string, string> {
parsedHeaders = JSON.parse(headers);
}
} catch (error) {
console.warn('Invalid OCO_API_CUSTOM_HEADERS format, ignoring custom headers');
console.warn(
'Invalid OCO_API_CUSTOM_HEADERS format, ignoring custom headers'
);
}
return parsedHeaders;
}
@@ -78,6 +82,12 @@ export function getEngine(): AiEngine {
case OCO_AI_PROVIDER_ENUM.DEEPSEEK:
return new DeepseekEngine(DEFAULT_CONFIG);
case OCO_AI_PROVIDER_ENUM.AIMLAPI:
return new AimlApiEngine(DEFAULT_CONFIG);
case OCO_AI_PROVIDER_ENUM.OPENROUTER:
return new OpenRouterEngine(DEFAULT_CONFIG);
default:
return new OpenAiEngine(DEFAULT_CONFIG);
}

View File

@@ -1,7 +1,7 @@
import { execa } from 'execa';
import { readFileSync } from 'fs';
import ignore, { Ignore } from 'ignore';
import { join } from 'path';
import { outro, spinner } from '@clack/prompts';
export const assertGitRepo = async () => {
@@ -16,41 +16,44 @@ export const assertGitRepo = async () => {
// (file) => `:(exclude)${file}`
// );
export const getOpenCommitIgnore = (): Ignore => {
export const getOpenCommitIgnore = async (): Promise<Ignore> => {
const gitDir = await getGitDir();
const ig = ignore();
try {
ig.add(readFileSync('.opencommitignore').toString().split('\n'));
ig.add(
readFileSync(join(gitDir, '.opencommitignore')).toString().split('\n')
);
} catch (e) {}
return ig;
};
export const getCoreHooksPath = async (): Promise<string> => {
const { stdout } = await execa('git', ['config', 'core.hooksPath']);
const gitDir = await getGitDir();
const { stdout } = await execa('git', ['config', 'core.hooksPath'], {
cwd: gitDir
});
return stdout;
};
export const getStagedFiles = async (): Promise<string[]> => {
const { stdout: gitDir } = await execa('git', [
'rev-parse',
'--show-toplevel'
]);
const gitDir = await getGitDir();
const { stdout: files } = await execa('git', [
'diff',
'--name-only',
'--cached',
'--relative',
gitDir
]);
const { stdout: files } = await execa(
'git',
['diff', '--name-only', '--cached', '--relative'],
{ cwd: gitDir }
);
if (!files) return [];
const filesList = files.split('\n');
const ig = getOpenCommitIgnore();
const ig = await getOpenCommitIgnore();
const allowedFiles = filesList.filter((file) => !ig.ignores(file));
if (!allowedFiles) return [];
@@ -59,12 +62,17 @@ export const getStagedFiles = async (): Promise<string[]> => {
};
export const getChangedFiles = async (): Promise<string[]> => {
const { stdout: modified } = await execa('git', ['ls-files', '--modified']);
const { stdout: others } = await execa('git', [
'ls-files',
'--others',
'--exclude-standard'
]);
const gitDir = await getGitDir();
const { stdout: modified } = await execa('git', ['ls-files', '--modified'], {
cwd: gitDir
});
const { stdout: others } = await execa(
'git',
['ls-files', '--others', '--exclude-standard'],
{ cwd: gitDir }
);
const files = [...modified.split('\n'), ...others.split('\n')].filter(
(file) => !!file
@@ -74,16 +82,20 @@ export const getChangedFiles = async (): Promise<string[]> => {
};
export const gitAdd = async ({ files }: { files: string[] }) => {
const gitDir = await getGitDir();
const gitAddSpinner = spinner();
gitAddSpinner.start('Adding files to commit');
await execa('git', ['add', ...files]);
await execa('git', ['add', ...files], { cwd: gitDir });
gitAddSpinner.stop(`Staged ${files.length} files`);
};
export const getDiff = async ({ files }: { files: string[] }) => {
const gitDir = await getGitDir();
const lockFiles = files.filter(
(file) =>
file.includes('.lock') ||
@@ -108,12 +120,20 @@ export const getDiff = async ({ files }: { files: string[] }) => {
(file) => !file.includes('.lock') && !file.includes('-lock.')
);
const { stdout: diff } = await execa('git', [
'diff',
'--staged',
'--',
...filesWithoutLocks
]);
const { stdout: diff } = await execa(
'git',
['diff', '--staged', '--', ...filesWithoutLocks],
{ cwd: gitDir }
);
return diff;
};
export const getGitDir = async (): Promise<string> => {
const { stdout: gitDir } = await execa('git', [
'rev-parse',
'--show-toplevel'
]);
return gitDir;
};

View File

@@ -4,20 +4,23 @@
* @param tag The tag name without angle brackets (e.g., 'think' for '<think></think>')
* @returns The content with the specified tags and their contents removed, and trimmed
*/
export function removeContentTags<T extends string | null | undefined>(content: T, tag: string): T {
export function removeContentTags<T extends string | null | undefined>(
content: T,
tag: string
): T {
if (!content || typeof content !== 'string') {
return content;
}
// Dynamic implementation for other cases
const openTag = `<${tag}>`;
const closeTag = `</${tag}>`;
// Parse the content and remove tags
let result = '';
let skipUntil: number | null = null;
let depth = 0;
for (let i = 0; i < content.length; i++) {
// Check for opening tag
if (content.substring(i, i + openTag.length) === openTag) {
@@ -29,7 +32,10 @@ export function removeContentTags<T extends string | null | undefined>(content:
}
}
// Check for closing tag
else if (content.substring(i, i + closeTag.length) === closeTag && depth > 0) {
else if (
content.substring(i, i + closeTag.length) === closeTag &&
depth > 0
) {
depth--;
if (depth === 0) {
i = i + closeTag.length - 1; // Skip the closing tag
@@ -37,7 +43,7 @@ export function removeContentTags<T extends string | null | undefined>(content:
continue;
}
}
// Only add character if not inside a tag
if (skipUntil === null) {
result += content[i];