Compare commits

...

108 Commits

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
di-sukharev
2540c169dc 3.2.8 2025-06-08 11:17:08 +03:00
di-sukharev
75147e91e7 refactor(git.ts): improve git add completion message for clarity 2025-06-08 10:42:07 +03:00
di-sukharev
59b6edb49c format 2025-06-08 10:41:16 +03:00
di-sukharev
7683004464 build 2025-06-08 10:41:13 +03:00
di-sukharev
e1f657939f chore(deps): bump esbuild from 0.15.18 to 0.25.5 and @actions/github from 5.1.1 to 6.0.1 2025-06-08 10:40:42 +03:00
GPT8
55904155a8 Merge pull request #472 from kakakakakku/fgm
feat(cli.ts): enhance fgm flag to include description and default value for better usability
2025-05-30 10:15:00 +03:00
GPT8
c1be5138b6 Merge pull request #477 from jonsguez/fix/one-line-commit
fix(prompts.ts): edited contradictory assistant output
2025-05-30 10:13:35 +03:00
GPT8
063aa94576 Merge pull request #476 from benleibowitz/master
feat(config): add 'describe' mode to config command for detailed parameter info
2025-05-30 10:11:24 +03:00
jonsguez
668e149ae3 fix(prompts.ts): edited contradictory assistant output
When user wants one line commits the system prompt and the user/assistant one-shot example were contradicting each other, confusing the LLM. This fix modifies the assistant output so that prompt and one-shot are consistent.
2025-05-29 23:09:10 -04:00
Ben Leibowitz
b5fca3155f feat(config): add 'describe' mode to config command for detailed parameter info
This commit adds a new 'describe' mode to the config command, allowing users
to get detailed information about configuration parameters. It includes:

1. New CONFIG_MODES.describe enum value
2. Functions to generate and print help messages for config parameters
3. Updated configCommand to handle the new 'describe' mode
4. README updates to document the new 'describe' functionality
2025-05-29 15:46:48 -04:00
GPT8
bc514f8f4d Merge pull request #461 from kaovilai/dep-updates
fix tests
2025-05-29 21:56:45 +03:00
Tiger Kaovilai
3b868ce6df fix: enable git push functionality in e2e tests
- Added OCO_GITPUSH='true' environment variable to all e2e test commands
- Fixed ESM module import issues in commitlint tests by using process.cwd()
- Replaced import.meta.url usage with relative paths from project root
- All e2e tests now properly test the git push prompt functionality
2025-05-29 12:05:28 -05:00
Tiger Kaovilai
aad62d4fa1 fix: remove duplicate modulePathIgnorePatterns in Jest config
- Fixed Jest configuration error by removing duplicate modulePathIgnorePatterns property
- Consolidated the ignore patterns into a single declaration
- This resolves the TypeScript compilation error preventing tests from running
2025-05-29 12:05:13 -05:00
GPT8
21e92164e7 Merge branch 'master' into dep-updates 2025-05-29 12:37:48 +03:00
kakakakakku
f0381c8b12 feat(cli.ts): enhance fgm flag to include description and default value for better usability 2025-05-19 09:04:31 +09:00
GPT8
f6de2dc775 Merge pull request #467 from EmilienMottet/master
Add OCO_API_CUSTOM_HEADERS
2025-05-03 12:06:21 +03:00
EmilienMottet
6aae1c7bd7 ♻️(engine): extract custom header parsing and update OpenAiEngine
- export parseCustomHeaders from src/utils/engine.ts
- use parseCustomHeaders in OpenAiEngine for config.customHeaders
- remove try/catch and inline JSON.parse logic
- update config test to expect headers as object and drop JSON.parse

Centralize header parsing for reuse and simplify engine code
Update tests to match new header format for clarity
2025-04-30 21:43:44 +02:00
EmilienMottet
71a44fac28 ♻️ refactor OpenAI client options and unify custom headers parsing
Use OpenAI.ClientOptions for stronger typing and clarity
Extract custom headers parsing into parseCustomHeaders util
Simplify getEngine by delegating header parsing to helper
Improve maintainability and reduce code duplication
2025-04-30 14:46:54 +02:00
EmilienMottet
6c48c935e2 add custom HTTP headers support via OCO_API_CUSTOM_HEADERS
Add OCO_API_CUSTOM_HEADERS variable to README, config enum,
and env parsing to allow JSON string of custom headers.
Validate that custom headers are valid JSON in config validator.
Extend AiEngineConfig with customHeaders and pass headers to
OllamaEngine and OpenAiEngine clients when creating requests.
Parse custom headers in utils/engine and warn on invalid format.
Add unit tests to ensure OCO_API_CUSTOM_HEADERS is handled
correctly and merged from env over global config.

This enables users to send additional headers such as
Authorization or tracing headers with LLM API calls.
2025-04-29 20:51:24 +02:00
GPT8
25c6a0d5d4 Merge pull request #462 from jcppkkk/master
refactor(commitlint): update commitlint configuration and prompts
2025-04-25 16:21:42 +03:00
GPT8
b277bf3d50 Merge pull request #465 from jcppkkk/fix-tests
Fix tests
2025-04-25 16:19:59 +03:00
Jethro Yu
83b6e0bbaf fix(removeContentTags): keep newlines to preserve formatting
The space normalization logic is updated to replace only multiple spaces
and tabs with a single space, while preserving newlines. This change
ensures that the formatting of the content is maintained, especially
when dealing with empty line requirements and max line length.
2025-04-25 18:44:06 +08:00
Jethro Yu
2726e51c2a fix(test): Stabilize Jest ESM configuration for CI
Resolves persistent `ReferenceError: exports is not defined` errors
encountered during unit tests (`unit-test (20.x)` job) in the GitHub
Actions CI environment. These errors occurred specifically when
importing `cli-testing-library` in the global Jest setup file
(`test/jest-setup.ts`), despite tests passing locally with the same
Node.js version (v20.19.0).

After iterative testing, the following Jest configuration combination
was identified as necessary to ensure consistent ESM handling and test
success in both local and CI environments:

- Set preset to `ts-jest/presets/default-esm` for stricter ESM rules.

- Configured `transformIgnorePatterns` to ensure Jest transforms specific ESM dependencies (`cli-testing-library`, `@clack`, `cleye`) within `node_modules`.

- Expanded the `transform` pattern (`^.+\\.(ts|tsx|js|jsx|mjs)$`) to explicitly cover various script types handled by `ts-jest`.

- Included explicit `tsconfig` overrides (`module: 'ESNext'`, `target: 'ES2022'`) within the `ts-jest` transform options to resolve potential environment discrepancies.

- Retained `moduleNameMapper` for `.js` imports for reliable module resolution.

- Ensured `cli-testing-library` imports remain in the global setup (`test/jest-setup.ts`).

- Removed test cache clearing from the `test:unit` script in `package.json`.

This configuration now passes reliably across environments.
2025-04-25 18:44:05 +08:00
Jethro Yu
da2742edb1 chore(package.json): add rimraf as a development dependency for improved file management
Including rimraf in the project allows for easier and more efficient
removal of files and directories, enhancing the development workflow,
especially for tasks like cleaning up build artifacts.
2025-04-25 18:44:05 +08:00
Jethro Yu
0ebff3b974 fix(removeContentTags): keep newlines to preserve formatting
The space normalization logic is updated to replace only multiple spaces
and tabs with a single space, while preserving newlines. This change
ensures that the formatting of the content is maintained, especially
when dealing with empty line requirements and max line length.
2025-04-25 18:40:50 +08:00
Jethro Yu
a52589e9fe fix(test): Stabilize Jest ESM configuration for CI
Resolves persistent `ReferenceError: exports is not defined` errors
encountered during unit tests (`unit-test (20.x)` job) in the GitHub
Actions CI environment. These errors occurred specifically when
importing `cli-testing-library` in the global Jest setup file
(`test/jest-setup.ts`), despite tests passing locally with the same
Node.js version (v20.19.0).

After iterative testing, the following Jest configuration combination
was identified as necessary to ensure consistent ESM handling and test
success in both local and CI environments:

- Set preset to `ts-jest/presets/default-esm` for stricter ESM rules.

- Configured `transformIgnorePatterns` to ensure Jest transforms specific ESM dependencies (`cli-testing-library`, `@clack`, `cleye`) within `node_modules`.

- Expanded the `transform` pattern (`^.+\\.(ts|tsx|js|jsx|mjs)$`) to explicitly cover various script types handled by `ts-jest`.

- Included explicit `tsconfig` overrides (`module: 'ESNext'`, `target: 'ES2022'`) within the `ts-jest` transform options to resolve potential environment discrepancies.

- Retained `moduleNameMapper` for `.js` imports for reliable module resolution.

- Ensured `cli-testing-library` imports remain in the global setup (`test/jest-setup.ts`).

- Removed test cache clearing from the `test:unit` script in `package.json`.

This configuration now passes reliably across environments.
2025-04-15 15:58:53 +08:00
Jethro Yu
5381c5e18b chore(package.json): add rimraf as a development dependency for improved file management
Including rimraf in the project allows for easier and more efficient
removal of files and directories, enhancing the development workflow,
especially for tasks like cleaning up build artifacts.
2025-04-15 14:54:06 +08:00
Jethro Yu
9ffcdbdb3b refactor(commitlint): update commitlint configuration and prompts for improved clarity and consistency
The commitlint configuration and prompts have been refactored to enhance
clarity and maintain consistency throughout the codebase. The type
assertion for commitLintConfig is updated to use 'as any' for better
type handling. Additionally, formatting adjustments are made in the
prompts to ensure proper readability and alignment with the defined
conventions. These changes aim to streamline the commit message
generation process and improve overall code maintainability.
2025-04-15 14:00:09 +08:00
Tiger Kaovilai
6bc1d90469 npm run test:unit passing locally
Signed-off-by: Tiger Kaovilai <passawit.kaovilai@gmail.com>
2025-04-14 05:06:45 -04:00
Tiger Kaovilai
5fb3d75412 fix tests
Signed-off-by: Tiger Kaovilai <passawit.kaovilai@gmail.com>
2025-04-14 04:57:53 -04:00
GPT8
b3700ae685 Merge pull request #458 from kaovilai/thiknagain
Extends #445 to other providers which many provides deepseek
2025-04-14 11:19:20 +03:00
GPT8
1d81229931 Merge pull request #459 from kaovilai/imp1
chore: update GitHub Actions workflows to use latest action versions and improve configurations
2025-04-14 11:18:29 +03:00
GPT8
22f96b34a5 Merge pull request #460 from Heyian/fix/punycode
Fix the punycode deprecated message from the source (survives building from eslint.config.ts).
2025-04-14 11:17:23 +03:00
Marc-Antoine Favreau
beecedf6f3 chore(deps): update eslint and typescript-eslint dependencies
feat(package.json): add overrides for ajv and whatwg-url packages
fix(deepseek.ts): correct import path for OpenAiEngine and OpenAiConfig
2025-04-12 11:36:12 -04:00
Tiger Kaovilai
566a9b1a52 reuse function
Signed-off-by: Tiger Kaovilai <passawit.kaovilai@gmail.com>
2025-04-12 04:51:11 -04:00
Tiger Kaovilai
aecc832529 chore: update GitHub Actions workflows to use latest action versions and improve configurations
Signed-off-by: Tiger Kaovilai <passawit.kaovilai@gmail.com>
2025-04-12 04:33:35 -04:00
GPT8
9418f67636 Merge pull request #457 from jcppkkk/feat/commitlint-improve-consistency-handling
refactor(prompts): streamline commit message generation logic
2025-04-12 10:40:30 +03:00
Tiger Kaovilai
f5c6c313fc Extends #445 to other providers which many provides deepseek 2025-04-11 12:50:16 -04:00
Jethro Yu
fb533f838d refactor(prompts): streamline commit message generation logic
This update introduces a new structure for generating commit messages by defining a constant for commit types and consolidating the logic into a single function. The changes enhance readability and maintainability while ensuring consistent formatting based on the OCO_OMIT_SCOPE configuration.
2025-04-10 16:16:26 +08:00
GPT8
60a7650e1c Merge pull request #456 from kvokka/fix-config-default-settings 2025-04-08 11:19:53 +03:00
Mikhail Beliakov
beb623cdcd fix: config default settings 2025-03-28 19:17:02 +00:00
GPT8
cd5198a96f Merge pull request #454 from jcppkkk/master
feat(commitlint): add additional search path for @commitlint
2025-03-17 11:40:01 +03:00
Jethro Yu
44bd14d2c5 feat(commitlint): add additional search path for @commitlint
This change allows opencommit to locate commitlint installed globally
(with opencommit), which is useful for project that does not use node.js
environment or dose not have opencommit and @commitlint installed locally.
2025-03-17 11:42:54 +08:00
di-sukharev
7feb3ec00e build 2025-03-11 11:37:11 +03:00
di-sukharev
ff896fc225 3.2.7 2025-03-11 11:37:10 +03:00
di-sukharev
6485b8381b 3.2.6 2025-03-11 11:36:57 +03:00
GPT8
7945f44259 Merge pull request #453 from bbauti/omit-scope-feature
feat(config): Add OCO_OMIT_SCOPE option to control scope inclusion in commit messages
2025-03-11 11:30:43 +03:00
di-sukharev
44a35da245 3.2.6 2025-03-11 11:30:21 +03:00
Bautista Igarzabal
f8ce0d32d5 feat(scope): add OMIT_SCOPE config option 2025-03-09 00:41:30 -03:00
Satoru Hoshino
b55bcd5c0b feat(engine): add DeepSeekEngine (#446)
Add DeepSeekEngine to support DeepSeek API.  This
includes a new DeepSeekConfig interface and updates to
the engine selection logic.

feat(README.md, src/commands/config.ts): Add DeepSeek support

Adds support for the DeepSeek AI provider.  Updates the README,
config validation, and model list to include DeepSeek.  This
allows users to utilize DeepSeek models with the OpenCommit
tool.

fix(deepseek.ts): update DeepSeek API base URL to include version number v1

refactor(deepseek.ts): improve DeepSeekEngine constructor

The DeepSeekEngine constructor is refactored to use the
spread syntax for better readability and maintainability
when merging config parameters.  The baseURL is now
explicitly set within the constructor.

fix(README.md): remove Groq from the list of supported AI providers

refactor(deepseek.ts): rename interface DeepseekConfig to DeepSeekEngineeekConfig and fix typo

Revert "refactor(deepseek.ts): rename interface DeepseekConfig to DeepSeekEngineeekConfig and fix typo"

This reverts commit f492367d3885fa97cd685feca889f93d6c465b2f.

refactor(deepseek.ts): Rename DeepseekConfig to DeepSeekConfig for consistency

 feat(engine): add DeepSeekEngine to support DeepSeek API

♻️ refactor(engine): improve OpenAiEngine and create a
    new DeepSeekEngine class to handle DeepSeek API
    requests.  The DeepSeekEngine class inherits from
    OpenAiEngine and overrides the
    generateCommitMessage method to use the DeepSeek
    API.  This change improves code organization and
    maintainability.

🐛 Fix: Correct DeepSeekEngine import and class name

The import path and class name for DeepSeekEngine were
incorrect, causing a runtime error. This commit corrects
the import path and class name to `DeepseekEngine` to
resolve the issue.

Revert "🐛 Fix: Correct DeepSeekEngine import and class name"

This reverts commit 738fd36c434d9df9c3a458b1e8230c974bd2a76e.

🐛 Fix: Correct DeepSeekEngine import and class name

The import path and class name for DeepSeekEngine were corrected to match the actual file and class name.  This fixes a runtime error.

Restore ./out directory to master state
2025-02-10 13:21:39 +03:00
Badraxas
6816379119 feat(engine/ollama.ts): extract message from response data and clean content by removing unnecessary tags (#445)
Co-authored-by: Bastien Mottier <b.mottier@viously.com>
2025-01-30 10:43:05 +01:00
di-sukharev
9d80991805 build 2024-12-14 20:07:57 +01:00
di-sukharev
2e1a39fd2f 3.2.5 2024-12-14 20:07:56 +01:00
di-sukharev
30ddd05764 jump to 3.2.4 2024-12-14 20:07:52 +01:00
di-sukharev
5fd84937c5 build 2024-12-14 20:05:17 +01:00
di-sukharev
dc4fe43642 build 2024-12-14 20:03:35 +01:00
di-sukharev
98afbe21ea 3.2.4 2024-12-14 20:03:34 +01:00
di-sukharev
041465a81c 3.2.3 2024-12-14 20:03:11 +01:00
GPT8
40fa275b4f 3.2.3 (#431)
* 378: fix hook env (#402)

* fix(prepare-commit-msg-hook): update error handling to provide clearer instructions for setting API keys and improve user guidance

* Fix: a bug that causes an error when pushing without setting git remote (#396)

* update deploy commands

* feat(cli): add context flag for providing additional commit message input

* Fix [Bug]: punycode` module is deprecated #426 (#433)

Signed-off-by: Tiger Kaovilai <passawit.kaovilai@gmail.com>

* npm audit fix (#432)

Signed-off-by: Tiger Kaovilai <passawit.kaovilai@gmail.com>

* Feat: Add an option to `Don't push` when there are multiple git remotes (#434)

---------

Co-authored-by: GPT8 <57486732+di-sukharev@users.noreply.github.com>

* feat(engine): add support for MLX AI provider (#437)

---------

Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
Co-authored-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>

* feat(config, engine): add support for Mistral AI provider and engine (#436)

* docs(CONTRIBUTING.md): update `TODO.md` reference (#435)

Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>

* feat(config, engine): add support for Mistral AI provider and engine

* ```
feat(package): add mistralai and zod dependencies
```

* fix: recreate package-lock.json with node20

* fix: recreate package-lock.json with node v20.18.1 based on branch dev

---------

Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
Co-authored-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
Co-authored-by: pedro-valentim <>

---------

Signed-off-by: Tiger Kaovilai <passawit.kaovilai@gmail.com>
Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
Co-authored-by: Takanori Matsumoto <matscube@gmail.com>
Co-authored-by: BILLY Maxime <ozeliurs@gmail.com>
Co-authored-by: Welington Sampaio <welington.sampaio@icloud.com>
Co-authored-by: Tiger Kaovilai <passawit.kaovilai@gmail.com>
Co-authored-by: albi ️ <sigismondi.alberto@gmail.com>
Co-authored-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
Co-authored-by: Pedro Valentim Silva Leite <18179935+pedro-valentim@users.noreply.github.com>
2024-12-14 20:02:25 +01:00
di-sukharev
6c9d89afea Merge branch 'master' into dev 2024-12-14 19:43:53 +01:00
Pedro Valentim Silva Leite
206887612a feat(config, engine): add support for Mistral AI provider and engine (#436)
* docs(CONTRIBUTING.md): update `TODO.md` reference (#435)

Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>

* feat(config, engine): add support for Mistral AI provider and engine

* ```
feat(package): add mistralai and zod dependencies
```

* fix: recreate package-lock.json with node20

* fix: recreate package-lock.json with node v20.18.1 based on branch dev

---------

Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
Co-authored-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
Co-authored-by: pedro-valentim <>
2024-12-11 14:01:50 +01:00
albi ⚡️
26ebfb416d feat(engine): add support for MLX AI provider (#437)
* docs(CONTRIBUTING.md): update `TODO.md` reference (#435)

Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>

* feat(engine): add support for MLX AI provider
docs/engine: update documentation to include new engine providers

* fix(mlx.ts): add repetition_penalty option to generateCommitMessage method for improved model behavior

---------

Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
Co-authored-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
2024-12-09 11:02:38 +01:00
Emmanuel Ferdman
6f16191af2 docs(CONTRIBUTING.md): update TODO.md reference (#435)
Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
2024-11-29 21:33:39 +01:00
Takanori Matsumoto
dd65b9c3e3 Feat: Add an option to Don't push when there are multiple git remotes (#434)
* feat(commit.ts): add option to skip pushing commits to a remote repository to enhance user flexibility during commit process

* Update src/commands/commit.ts

---------

Co-authored-by: GPT8 <57486732+di-sukharev@users.noreply.github.com>
2024-11-28 13:56:01 +01:00
Tiger Kaovilai
6b822eb6d1 npm audit fix (#432)
Signed-off-by: Tiger Kaovilai <passawit.kaovilai@gmail.com>
2024-11-20 11:49:57 +01:00
Tiger Kaovilai
809f5ff5f7 Fix [Bug]: punycode` module is deprecated #426 (#433)
Signed-off-by: Tiger Kaovilai <passawit.kaovilai@gmail.com>
2024-11-20 11:49:23 +01:00
Welington Sampaio
eca4083a04 Feat/add user input code context (#420)
* feat(cli): add context flag for providing additional commit message input
2024-11-20 11:47:58 +01:00
di-sukharev
0f315ae793 Merge remote-tracking branch 'origin/master' into dev 2024-11-19 18:59:27 +01:00
di-sukharev
25105e4c3a docs(CONTRIBUTING.md): update links to point to the correct repository name for consistency and clarity 2024-09-07 19:11:37 +03:00
GPT8
2769121842 3.2.2 (#413)
* feat(config): add support for groq AI provider, including config validation and engine implementation (#381)

* fix migrations (#414)

---------

Co-authored-by: Takanori Matsumoto <matscube@gmail.com>
Co-authored-by: BILLY Maxime <ozeliurs@gmail.com>
2024-09-07 18:17:17 +03:00
GPT8
31200609b6 fix migrations (#414)
* refactor(config.ts): improve code readability by formatting array elements and conditions
* fix(migrations): handle undefined values correctly when setting default config values
* fix(migrations): ensure process exits with error code on migration failure
* fix(commit.ts): update error handling to provide clearer feedback when commit message generation fails
* feat(config.ts): add cleanUndefinedValues function to sanitize config values by converting 'undefined' and 'null' strings to actual values
* refactor(config.ts): return cleaned config from getConfig function to ensure consistent data types
* chore(migrations): log entriesToSet in migration to assist with debugging and tracking changes
2024-09-07 18:10:15 +03:00
di-sukharev
03b570c85c 3.2.2 2024-09-07 15:16:19 +03:00
di-sukharev
e3529e9ca7 Merge remote-tracking branch 'origin/master' into dev 2024-09-07 15:16:10 +03:00
BILLY Maxime
2d7e3842d6 feat(config): add support for groq AI provider, including config validation and engine implementation (#381) 2024-09-07 15:03:25 +03:00
di-sukharev
8ae927e2dc build 2024-09-06 13:59:28 +03:00
di-sukharev
a91aa3b4de update deploy commands 2024-09-06 12:17:37 +03:00
di-sukharev
f46336b86a 3.2.0 2024-09-06 12:15:18 +03:00
GPT8
f975e49760 refactoring v2 (#408) 2024-09-06 12:14:15 +03:00
Takanori Matsumoto
fa1cf46050 Fix: a bug that causes an error when pushing without setting git remote (#396) 2024-09-04 18:42:54 +03:00
GPT10
1d19ddd9e2 378: fix hook env (#402)
* fix(prepare-commit-msg-hook): update error handling to provide clearer instructions for setting API keys and improve user guidance

* build
2024-09-03 13:14:45 +03:00
65 changed files with 60539 additions and 13334 deletions

View File

@@ -18,7 +18,7 @@ To get started, follow these steps:
1. Clone the project repository locally.
2. Install dependencies with `npm install`.
3. Run the project with `npm run dev`.
4. See [issues](https://github.com/di-sukharev/open-commit/issues) or [TODO.md](../TODO.md) to help the project.
4. See [issues](https://github.com/di-sukharev/opencommit/issues) or [TODO.md](TODO.md) to help the project.
## Commit message guidelines
@@ -30,7 +30,7 @@ If you encounter any issues while using the project, please report them on the G
## Contacts
If you have any questions about contributing to the project, please contact by [creating an issue](https://github.com/di-sukharev/open-commit/issues) on the GitHub issue tracker.
If you have any questions about contributing to the project, please contact by [creating an issue](https://github.com/di-sukharev/opencommit/issues) on the GitHub issue tracker.
## License

View File

@@ -40,11 +40,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -58,7 +58,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -71,6 +71,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

View File

@@ -15,6 +15,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: 'Dependency Review'
uses: actions/dependency-review-action@v2
uses: actions/dependency-review-action@v3

View File

@@ -1,6 +1,11 @@
name: Testing
on: [pull_request]
on:
pull_request:
push:
branches:
- master
- main
jobs:
unit-test:
@@ -9,11 +14,12 @@ jobs:
matrix:
node-version: [20.x]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm install
- name: Run Unit Tests
@@ -24,11 +30,12 @@ jobs:
matrix:
node-version: [20.x]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install git
run: |
sudo apt-get update
@@ -44,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

3
.gitignore vendored
View File

@@ -11,4 +11,5 @@ uncaughtExceptions.log
src/*.json
.idea
test.ts
notes.md
notes.md
.nvmrc

View File

@@ -106,14 +106,15 @@ Create a `.env` file and add OpenCommit config variables there like this:
```env
...
OCO_AI_PROVIDER=<openai (default), anthropic, azure, ollama, gemini, flowise>
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>
OCO_TOKENS_MAX_INPUT=<max model token limit (default: 4096)>
OCO_TOKENS_MAX_OUTPUT=<max response tokens (default: 500)>
OCO_DESCRIPTION=<postface a message with ~3 sentences description of the changes>
OCO_EMOJI=<boolean, add GitMoji>
OCO_MODEL=<either 'gpt-4o', 'gpt-4', 'gpt-4-turbo', 'gpt-3.5-turbo' (default), 'gpt-3.5-turbo-0125', 'gpt-4-1106-preview', 'gpt-4-turbo-preview' or 'gpt-4-0125-preview' or any Anthropic or Ollama model or any string basically, but it should be a valid model name>
OCO_MODEL=<either 'gpt-4o-mini' (default), 'gpt-4o', 'gpt-4', 'gpt-4-turbo', 'gpt-3.5-turbo', 'gpt-3.5-turbo-0125', 'gpt-4-1106-preview', 'gpt-4-turbo-preview' or 'gpt-4-0125-preview' or any Anthropic or Ollama model or any string basically, but it should be a valid model name>
OCO_LANGUAGE=<locale, scroll to the bottom to see options>
OCO_MESSAGE_TEMPLATE_PLACEHOLDER=<message template placeholder, default: '$msg'>
OCO_PROMPT_MODULE=<either conventional-commit or @commitlint, default: conventional-commit>
@@ -132,6 +133,18 @@ Simply set any of the variables above like this:
oco config set OCO_MODEL=gpt-4o-mini
```
To see all available configuration parameters and their accepted values:
```sh
oco config describe
```
To see details for a specific parameter:
```sh
oco config describe OCO_MODEL
```
Configure [GitMoji](https://gitmoji.dev/) to preface a message.
```sh

View File

@@ -9,19 +9,33 @@ const config: Config = {
testTimeout: 100_000,
coverageProvider: 'v8',
moduleDirectories: ['node_modules', 'src'],
preset: 'ts-jest/presets/js-with-ts-esm',
preset: 'ts-jest/presets/default-esm',
setupFilesAfterEnv: ['<rootDir>/test/jest-setup.ts'],
testEnvironment: 'node',
testRegex: ['.*\\.test\\.ts$'],
transformIgnorePatterns: ['node_modules/(?!cli-testing-library)'],
// Tell Jest to ignore the specific duplicate package.json files
// that are causing Haste module naming collisions
modulePathIgnorePatterns: [
'<rootDir>/test/e2e/prompt-module/data/'
],
transformIgnorePatterns: [
'node_modules/(?!(cli-testing-library|@clack|cleye)/.*)'
],
transform: {
'^.+\\.(ts|tsx)$': [
'^.+\\.(ts|tsx|js|jsx|mjs)$': [
'ts-jest',
{
diagnostics: false,
useESM: true
useESM: true,
tsconfig: {
module: 'ESNext',
target: 'ES2022'
}
}
]
},
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1'
}
};

28817
out/cli.cjs

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4865
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "opencommit",
"version": "3.2.1",
"version": "3.2.10",
"description": "Auto-generate impressive commits in 1 second. Killing lame commits with AI 🤯🔫",
"keywords": [
"git",
@@ -17,11 +17,11 @@
],
"main": "cli.js",
"bin": {
"opencommit": "./out/cli.cjs",
"oco": "./out/cli.cjs"
"opencommit": "out/cli.cjs",
"oco": "out/cli.cjs"
},
"repository": {
"url": "https://github.com/di-sukharev/opencommit"
"url": "git+https://github.com/di-sukharev/opencommit.git"
},
"type": "module",
"author": "https://github.com/di-sukharev",
@@ -44,13 +44,14 @@
"ollama:start": "OCO_AI_PROVIDER='ollama' node ./out/cli.cjs",
"dev": "ts-node ./src/cli.ts",
"dev:gemini": "OCO_AI_PROVIDER='gemini' ts-node ./src/cli.ts",
"build": "rimraf out && node esbuild.config.js",
"build": "npx rimraf out && node esbuild.config.js",
"build:push": "npm run build && git add . && git commit -m 'build' && git push",
"deploy": "npm publish --tag latest",
"deploy:build": "npm run build:push && git push --tags && npm run deploy",
"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 .",
@@ -58,7 +59,8 @@
"test:unit:docker": "npm run test:docker-build && DOCKER_CONTENT_TRUST=0 docker run --rm oco-test npm run test:unit",
"test:e2e": "npm run test:e2e:setup && jest test/e2e",
"test:e2e:setup": "sh test/e2e/setup.sh",
"test:e2e:docker": "npm run test:docker-build && DOCKER_CONTENT_TRUST=0 docker run --rm oco-test npm run test:e2e"
"test:e2e:docker": "npm run test:docker-build && DOCKER_CONTENT_TRUST=0 docker run --rm oco-test npm run test:e2e",
"mlx:start": "OCO_AI_PROVIDER='mlx' node ./out/cli.cjs"
},
"devDependencies": {
"@commitlint/types": "^17.4.4",
@@ -66,14 +68,15 @@
"@types/inquirer": "^9.0.3",
"@types/jest": "^29.5.12",
"@types/node": "^16.18.14",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"@typescript-eslint/eslint-plugin": "^8.29.0",
"@typescript-eslint/parser": "^8.29.0",
"cli-testing-library": "^2.0.2",
"dotenv": "^16.0.3",
"esbuild": "^0.15.18",
"eslint": "^8.28.0",
"esbuild": "^0.25.5",
"eslint": "^9.24.0",
"jest": "^29.7.0",
"prettier": "^2.8.4",
"rimraf": "^6.0.1",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.1",
"typescript": "^4.9.3"
@@ -81,12 +84,13 @@
"dependencies": {
"@actions/core": "^1.10.0",
"@actions/exec": "^1.1.1",
"@actions/github": "^5.1.1",
"@actions/github": "^6.0.1",
"@anthropic-ai/sdk": "^0.19.2",
"@azure/openai": "^1.0.0-beta.12",
"@clack/prompts": "^0.6.1",
"@dqbd/tiktoken": "^1.0.2",
"@google/generative-ai": "^0.11.4",
"@mistralai/mistralai": "^1.3.5",
"@octokit/webhooks-schemas": "^6.11.0",
"@octokit/webhooks-types": "^6.11.0",
"axios": "^1.3.4",
@@ -97,6 +101,12 @@
"ignore": "^5.2.4",
"ini": "^3.0.1",
"inquirer": "^9.1.4",
"openai": "^4.57.0"
"openai": "^4.57.0",
"punycode": "^2.3.1",
"zod": "^3.23.8"
},
"overrides": {
"ajv": "^8.17.1",
"whatwg-url": "^14.0.0"
}
}

View File

@@ -19,7 +19,17 @@ cli(
name: 'opencommit',
commands: [configCommand, hookCommand, commitlintConfigCommand],
flags: {
fgm: Boolean,
fgm: {
type: Boolean,
description: 'Use full GitMoji specification',
default: false
},
context: {
type: String,
alias: 'c',
description: 'Additional user input context for the commit message',
default: ''
},
yes: {
type: Boolean,
alias: 'y',
@@ -37,7 +47,7 @@ cli(
if (await isHookCalled()) {
prepareCommitMessageHook();
} else {
commit(extraArgs, false, flags.fgm, flags.yes);
commit(extraArgs, flags.context, false, flags.fgm, flags.yes);
}
},
extraArgs

View File

@@ -1,4 +1,5 @@
import {
text,
confirm,
intro,
isCancel,
@@ -39,6 +40,7 @@ const checkMessageTemplate = (extraArgs: string[]): string | false => {
interface GenerateCommitMessageFromGitDiffParams {
diff: string;
extraArgs: string[];
context?: string;
fullGitMojiSpec?: boolean;
skipCommitConfirmation?: boolean;
}
@@ -46,6 +48,7 @@ interface GenerateCommitMessageFromGitDiffParams {
const generateCommitMessageFromGitDiff = async ({
diff,
extraArgs,
context = '',
fullGitMojiSpec = false,
skipCommitConfirmation = false
}: GenerateCommitMessageFromGitDiffParams): Promise<void> => {
@@ -56,7 +59,8 @@ const generateCommitMessageFromGitDiff = async ({
try {
let commitMessage = await generateCommitMessageByDiff(
diff,
fullGitMojiSpec
fullGitMojiSpec,
context
);
const messageTemplate = checkMessageTemplate(extraArgs);
@@ -82,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', [
@@ -146,26 +164,32 @@ ${chalk.grey('——————————————————')}`
process.exit(0);
}
} else {
const skipOption = `don't push`;
const selectedRemote = (await select({
message: 'Choose a remote to push to',
options: remotes.map((remote) => ({ value: remote, label: remote }))
options: [...remotes, skipOption].map((remote) => ({
value: remote,
label: remote
}))
})) as string;
if (isCancel(selectedRemote)) process.exit(1);
const pushSpinner = spinner();
if (selectedRemote !== skipOption) {
const pushSpinner = spinner();
pushSpinner.start(`Running 'git push ${selectedRemote}'`);
pushSpinner.start(`Running 'git push ${selectedRemote}'`);
const { stdout } = await execa('git', ['push', selectedRemote]);
const { stdout } = await execa('git', ['push', selectedRemote]);
if (stdout) outro(stdout);
if (stdout) outro(stdout);
pushSpinner.stop(
`${chalk.green(
'✔'
)} successfully pushed all commits to ${selectedRemote}`
);
pushSpinner.stop(
`${chalk.green(
'✔'
)} successfully pushed all commits to ${selectedRemote}`
);
}
}
} else {
const regenerateMessage = await confirm({
@@ -183,7 +207,11 @@ ${chalk.grey('——————————————————')}`
}
}
} catch (error) {
commitGenerationSpinner.stop('📝 Commit message generated');
commitGenerationSpinner.stop(
`${chalk.red('✖')} Failed to generate the commit message`
);
console.log(error);
const err = error as Error;
outro(`${chalk.red('✖')} ${err?.message || err}`);
@@ -193,6 +221,7 @@ ${chalk.grey('——————————————————')}`
export async function commit(
extraArgs: string[] = [],
context: string = '',
isStageAllFlag: Boolean = false,
fullGitMojiSpec: boolean = false,
skipCommitConfirmation: boolean = false
@@ -225,8 +254,9 @@ export async function commit(
stagedFilesSpinner.start('Counting staged files');
if (!stagedFiles.length) {
if (stagedFiles.length === 0) {
stagedFilesSpinner.stop('No files are staged');
const isStageAllAndCommitConfirmedByUser = await confirm({
message: 'Do you want to stage all files and generate commit message?'
});
@@ -234,8 +264,8 @@ export async function commit(
if (isCancel(isStageAllAndCommitConfirmedByUser)) process.exit(1);
if (isStageAllAndCommitConfirmedByUser) {
await commit(extraArgs, true, fullGitMojiSpec);
process.exit(1);
await commit(extraArgs, context, true, fullGitMojiSpec);
process.exit(0);
}
if (stagedFiles.length === 0 && changedFiles.length > 0) {
@@ -247,13 +277,13 @@ export async function commit(
}))
})) as string[];
if (isCancel(files)) process.exit(1);
if (isCancel(files)) process.exit(0);
await gitAdd({ files });
}
await commit(extraArgs, false, fullGitMojiSpec);
process.exit(1);
await commit(extraArgs, context, false, fullGitMojiSpec);
process.exit(0);
}
stagedFilesSpinner.stop(
@@ -266,6 +296,7 @@ export async function commit(
generateCommitMessageFromGitDiff({
diff: await getDiff({ files: stagedFiles }),
extraArgs,
context,
fullGitMojiSpec,
skipCommitConfirmation
})

View File

@@ -25,12 +25,16 @@ export enum CONFIG_KEYS {
OCO_ONE_LINE_COMMIT = 'OCO_ONE_LINE_COMMIT',
OCO_TEST_MOCK_TYPE = 'OCO_TEST_MOCK_TYPE',
OCO_API_URL = 'OCO_API_URL',
OCO_GITPUSH = 'OCO_GITPUSH' // todo: deprecate
OCO_API_CUSTOM_HEADERS = 'OCO_API_CUSTOM_HEADERS',
OCO_OMIT_SCOPE = 'OCO_OMIT_SCOPE',
OCO_GITPUSH = 'OCO_GITPUSH', // todo: deprecate
OCO_HOOK_AUTO_UNCOMMENT = 'OCO_HOOK_AUTO_UNCOMMENT'
}
export enum CONFIG_MODES {
get = 'get',
set = 'set'
set = 'set',
describe = 'describe'
}
export const MODEL_LIST = {
@@ -76,6 +80,495 @@ export const MODEL_LIST = {
'gemini-1.0-pro',
'gemini-pro-vision',
'text-embedding-004'
],
groq: [
'llama3-70b-8192', // Meta Llama 3 70B (default one, no daily token limit and 14 400 reqs/day)
'llama3-8b-8192', // Meta Llama 3 8B
'llama-guard-3-8b', // Llama Guard 3 8B
'llama-3.1-8b-instant', // Llama 3.1 8B (Preview)
'llama-3.1-70b-versatile', // Llama 3.1 70B (Preview)
'gemma-7b-it', // Gemma 7B
'gemma2-9b-it' // Gemma 2 9B
],
mistral: [
'ministral-3b-2410',
'ministral-3b-latest',
'ministral-8b-2410',
'ministral-8b-latest',
'open-mistral-7b',
'mistral-tiny',
'mistral-tiny-2312',
'open-mistral-nemo',
'open-mistral-nemo-2407',
'mistral-tiny-2407',
'mistral-tiny-latest',
'open-mixtral-8x7b',
'mistral-small',
'mistral-small-2312',
'open-mixtral-8x22b',
'open-mixtral-8x22b-2404',
'mistral-small-2402',
'mistral-small-2409',
'mistral-small-latest',
'mistral-medium-2312',
'mistral-medium',
'mistral-medium-latest',
'mistral-large-2402',
'mistral-large-2407',
'mistral-large-2411',
'mistral-large-latest',
'pixtral-large-2411',
'pixtral-large-latest',
'codestral-2405',
'codestral-latest',
'codestral-mamba-2407',
'open-codestral-mamba',
'codestral-mamba-latest',
'pixtral-12b-2409',
'pixtral-12b',
'pixtral-12b-latest',
'mistral-embed',
'mistral-moderation-2411',
'mistral-moderation-latest'
],
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'
]
};
@@ -83,18 +576,30 @@ const getDefaultModel = (provider: string | undefined): string => {
switch (provider) {
case 'ollama':
return '';
case 'mlx':
return '';
case 'anthropic':
return MODEL_LIST.anthropic[0];
case 'gemini':
return MODEL_LIST.gemini[0];
case 'groq':
return MODEL_LIST.groq[0];
case 'mistral':
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];
}
};
export enum DEFAULT_TOKEN_LIMITS {
DEFAULT_MAX_TOKENS_INPUT = 40960,
DEFAULT_MAX_TOKENS_OUTPUT = 4096
DEFAULT_MAX_TOKENS_INPUT = 4096,
DEFAULT_MAX_TOKENS_OUTPUT = 500
}
const validateConfig = (
@@ -126,7 +631,7 @@ export const configValidators = {
validateConfig(
'OCO_API_KEY',
value,
'You need to provide the OCO_API_KEY when OCO_AI_PROVIDER set to "openai" (default) or "ollama" or "azure" or "gemini" or "flowise" or "anthropic". Run `oco config set OCO_API_KEY=your_key OCO_AI_PROVIDER=openai`'
'You need to provide the OCO_API_KEY when OCO_AI_PROVIDER set to "openai" (default) or "ollama" or "mlx" or "azure" or "gemini" or "flowise" or "anthropic" or "deepseek". Run `oco config set OCO_API_KEY=your_key OCO_AI_PROVIDER=openai`'
);
return value;
@@ -142,6 +647,22 @@ export const configValidators = {
return value;
},
[CONFIG_KEYS.OCO_API_CUSTOM_HEADERS](value) {
try {
// Custom headers must be a valid JSON string
if (typeof value === 'string') {
JSON.parse(value);
}
return value;
} catch (error) {
validateConfig(
CONFIG_KEYS.OCO_API_CUSTOM_HEADERS,
false,
'Must be a valid JSON string of headers'
);
}
},
[CONFIG_KEYS.OCO_TOKENS_MAX_INPUT](value: any) {
value = parseInt(value);
validateConfig(
@@ -174,6 +695,16 @@ export const configValidators = {
return value;
},
[CONFIG_KEYS.OCO_OMIT_SCOPE](value: any) {
validateConfig(
CONFIG_KEYS.OCO_OMIT_SCOPE,
typeof value === 'boolean',
'Must be boolean: true or false'
);
return value;
},
[CONFIG_KEYS.OCO_LANGUAGE](value: any) {
const supportedLanguages = Object.keys(i18n);
@@ -241,10 +772,20 @@ export const configValidators = {
validateConfig(
CONFIG_KEYS.OCO_AI_PROVIDER,
['openai', 'anthropic', 'gemini', 'azure', 'test', 'flowise'].includes(
value
) || value.startsWith('ollama'),
`${value} is not supported yet, use 'ollama', 'anthropic', 'azure', 'gemini', 'flowise' or 'openai' (default)`
[
'openai',
'mistral',
'anthropic',
'gemini',
'azure',
'test',
'flowise',
'groq',
'deepseek',
'aimlapi',
'openrouter'
].includes(value) || value.startsWith('ollama'),
`${value} is not supported yet, use 'ollama', 'mlx', 'anthropic', 'azure', 'gemini', 'flowise', 'mistral', 'deepseek', 'aimlapi' or 'openai' (default)`
);
return value;
@@ -278,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'
);
}
};
@@ -288,7 +837,13 @@ export enum OCO_AI_PROVIDER_ENUM {
GEMINI = 'gemini',
AZURE = 'azure',
TEST = 'test',
FLOWISE = 'flowise'
FLOWISE = 'flowise',
GROQ = 'groq',
MISTRAL = 'mistral',
MLX = 'mlx',
DEEPSEEK = 'deepseek',
AIMLAPI = 'aimlapi',
OPENROUTER = 'openrouter'
}
export type ConfigType = {
@@ -296,6 +851,7 @@ export type ConfigType = {
[CONFIG_KEYS.OCO_TOKENS_MAX_INPUT]: number;
[CONFIG_KEYS.OCO_TOKENS_MAX_OUTPUT]: number;
[CONFIG_KEYS.OCO_API_URL]?: string;
[CONFIG_KEYS.OCO_API_CUSTOM_HEADERS]?: string;
[CONFIG_KEYS.OCO_DESCRIPTION]: boolean;
[CONFIG_KEYS.OCO_EMOJI]: boolean;
[CONFIG_KEYS.OCO_WHY]: boolean;
@@ -306,7 +862,9 @@ export type ConfigType = {
[CONFIG_KEYS.OCO_AI_PROVIDER]: OCO_AI_PROVIDER_ENUM;
[CONFIG_KEYS.OCO_GITPUSH]: boolean;
[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');
@@ -352,9 +910,10 @@ export const DEFAULT_CONFIG = {
OCO_AI_PROVIDER: OCO_AI_PROVIDER_ENUM.OPENAI,
OCO_ONE_LINE_COMMIT: false,
OCO_TEST_MOCK_TYPE: 'commit-message',
OCO_FLOWISE_ENDPOINT: ':',
OCO_WHY: false,
OCO_GITPUSH: true // todo: deprecate
OCO_OMIT_SCOPE: false,
OCO_GITPUSH: true, // todo: deprecate
OCO_HOOK_AUTO_UNCOMMENT: false
};
const initGlobalConfig = (configPath: string = defaultConfigPath) => {
@@ -377,6 +936,7 @@ const getEnvConfig = (envPath: string) => {
OCO_MODEL: process.env.OCO_MODEL,
OCO_API_URL: process.env.OCO_API_URL,
OCO_API_KEY: process.env.OCO_API_KEY,
OCO_API_CUSTOM_HEADERS: process.env.OCO_API_CUSTOM_HEADERS,
OCO_AI_PROVIDER: process.env.OCO_AI_PROVIDER as OCO_AI_PROVIDER_ENUM,
OCO_TOKENS_MAX_INPUT: parseConfigVarValue(process.env.OCO_TOKENS_MAX_INPUT),
@@ -392,6 +952,7 @@ const getEnvConfig = (envPath: string) => {
OCO_PROMPT_MODULE: process.env.OCO_PROMPT_MODULE as OCO_PROMPT_MODULE_ENUM,
OCO_ONE_LINE_COMMIT: parseConfigVarValue(process.env.OCO_ONE_LINE_COMMIT),
OCO_TEST_MOCK_TYPE: process.env.OCO_TEST_MOCK_TYPE,
OCO_OMIT_SCOPE: parseConfigVarValue(process.env.OCO_OMIT_SCOPE),
OCO_GITPUSH: parseConfigVarValue(process.env.OCO_GITPUSH) // todo: deprecate
};
@@ -444,6 +1005,25 @@ interface GetConfigOptions {
setDefaultValues?: boolean;
}
const cleanUndefinedValues = (config: ConfigType) => {
return Object.fromEntries(
Object.entries(config).map(([_, v]) => {
try {
if (typeof v === 'string') {
if (v === 'undefined') return [_, undefined];
if (v === 'null') return [_, null];
const parsedValue = JSON.parse(v);
return [_, parsedValue];
}
return [_, v];
} catch (error) {
return [_, v];
}
})
);
};
export const getConfig = ({
envPath = defaultEnvPath,
globalPath = defaultConfigPath
@@ -453,7 +1033,9 @@ export const getConfig = ({
const config = mergeConfigs(envConfig, globalConfig);
return config;
const cleanConfig = cleanUndefinedValues(config);
return cleanConfig as ConfigType;
};
export const setConfig = (
@@ -496,28 +1078,220 @@ export const setConfig = (
outro(`${chalk.green('✔')} config successfully set`);
};
// --- HELP MESSAGE GENERATION ---
function getConfigKeyDetails(key) {
switch (key) {
case CONFIG_KEYS.OCO_MODEL:
return {
description: 'The AI model to use for generating commit messages',
values: MODEL_LIST
};
case CONFIG_KEYS.OCO_AI_PROVIDER:
return {
description: 'The AI provider to use',
values: Object.values(OCO_AI_PROVIDER_ENUM)
};
case CONFIG_KEYS.OCO_PROMPT_MODULE:
return {
description: 'The prompt module to use for commit message generation',
values: Object.values(OCO_PROMPT_MODULE_ENUM)
};
case CONFIG_KEYS.OCO_LANGUAGE:
return {
description: 'The locale to use for commit messages',
values: Object.keys(i18n)
};
case CONFIG_KEYS.OCO_TEST_MOCK_TYPE:
return {
description: 'The type of test mock to use',
values: ['commit-message', 'prompt-module-commitlint-config']
};
case CONFIG_KEYS.OCO_ONE_LINE_COMMIT:
return {
description: 'One line commit message',
values: ['true', 'false']
};
case CONFIG_KEYS.OCO_DESCRIPTION:
return {
description:
'Postface a message with ~3 sentences description of the changes',
values: ['true', 'false']
};
case CONFIG_KEYS.OCO_EMOJI:
return {
description: 'Preface a message with GitMoji',
values: ['true', 'false']
};
case CONFIG_KEYS.OCO_WHY:
return {
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',
values: ['true', 'false']
};
case CONFIG_KEYS.OCO_GITPUSH:
return {
description:
'Push to git after commit (deprecated). If false, oco will exit after committing',
values: ['true', 'false']
};
case CONFIG_KEYS.OCO_TOKENS_MAX_INPUT:
return {
description: 'Max model token limit',
values: ['Any positive integer']
};
case CONFIG_KEYS.OCO_TOKENS_MAX_OUTPUT:
return {
description: 'Max response tokens',
values: ['Any positive integer']
};
case CONFIG_KEYS.OCO_API_KEY:
return {
description: 'API key for the selected provider',
values: ['String (required for most providers)']
};
case CONFIG_KEYS.OCO_API_URL:
return {
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 $)']
};
case CONFIG_KEYS.OCO_HOOK_AUTO_UNCOMMENT:
return {
description: 'Automatically uncomment the commit message in the hook',
values: ['true', 'false']
};
default:
return {
description: 'String value',
values: ['Any string']
};
}
}
function printConfigKeyHelp(param) {
if (!Object.values(CONFIG_KEYS).includes(param)) {
console.log(chalk.red(`Unknown config parameter: ${param}`));
return;
}
const details = getConfigKeyDetails(param as CONFIG_KEYS);
let desc = details.description;
let defaultValue = undefined;
if (param in DEFAULT_CONFIG) {
defaultValue = DEFAULT_CONFIG[param];
}
console.log(chalk.bold(`\n${param}:`));
console.log(chalk.gray(` Description: ${desc}`));
if (defaultValue !== undefined) {
// Print booleans and numbers as-is, strings without quotes
if (typeof defaultValue === 'string') {
console.log(chalk.gray(` Default: ${defaultValue}`));
} else {
console.log(chalk.gray(` Default: ${defaultValue}`));
}
}
if (Array.isArray(details.values)) {
console.log(chalk.gray(' Accepted values:'));
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) => {
console.log(chalk.gray(` - ${value}`));
});
});
}
}
function printAllConfigHelp() {
console.log(chalk.bold('Available config parameters:'));
for (const key of Object.values(CONFIG_KEYS).sort()) {
const details = getConfigKeyDetails(key);
// Try to get the default value from DEFAULT_CONFIG
let defaultValue = undefined;
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) {
if (typeof defaultValue === 'string') {
console.log(chalk.gray(` Default: ${defaultValue}`));
} else {
console.log(chalk.gray(` Default: ${defaultValue}`));
}
}
}
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(
{
name: COMMANDS.config,
parameters: ['<mode>', '<key=values...>']
parameters: ['<mode>', '[key=values...]'],
help: {
description: 'Configure opencommit settings',
examples: [
'Describe all config parameters: oco config describe',
'Describe a specific parameter: oco config describe OCO_MODEL',
'Get a config value: oco config get OCO_MODEL',
'Set a config value: oco config set OCO_MODEL=gpt-4'
]
}
},
async (argv) => {
try {
const { mode, keyValues } = argv._;
intro(`COMMAND: config ${mode} ${keyValues}`);
if (mode === CONFIG_MODES.get) {
if (mode === CONFIG_MODES.describe) {
if (!keyValues || keyValues.length === 0) {
printAllConfigHelp();
} else {
for (const key of keyValues) {
printConfigKeyHelp(key);
}
}
process.exit(0);
} else if (mode === CONFIG_MODES.get) {
if (!keyValues || keyValues.length === 0) {
throw new Error('No config keys specified for get mode');
}
const config = getConfig() || {};
for (const key of keyValues) {
outro(`${key}=${config[key as keyof typeof config]}`);
}
} else if (mode === CONFIG_MODES.set) {
if (!keyValues || keyValues.length === 0) {
throw new Error('No config keys specified for set mode');
}
await setConfig(
keyValues.map((keyValue) => keyValue.split('=') as [string, string])
);
} else {
throw new Error(
`Unsupported mode: ${mode}. Valid modes are: "set" and "get"`
`Unsupported mode: ${mode}. Valid modes are: "set", "get", and "describe"`
);
}
} catch (error) {

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);

View File

@@ -3,6 +3,7 @@ import { OpenAIClient as AzureOpenAIClient } from '@azure/openai';
import { GoogleGenerativeAI as GeminiClient } from '@google/generative-ai';
import { AxiosInstance as RawAxiosClient } from 'axios';
import { OpenAI as OpenAIClient } from 'openai';
import { Mistral as MistralClient } from '@mistralai/mistralai';
export interface AiEngineConfig {
apiKey: string;
@@ -10,6 +11,7 @@ export interface AiEngineConfig {
maxTokensOutput: number;
maxTokensInput: number;
baseURL?: string;
customHeaders?: Record<string, string>;
}
type Client =
@@ -17,7 +19,8 @@ type Client =
| AzureOpenAIClient
| AnthropicClient
| RawAxiosClient
| GeminiClient;
| GeminiClient
| MistralClient;
export interface AiEngine {
config: AiEngineConfig;

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

@@ -8,6 +8,7 @@ import axios from 'axios';
import chalk from 'chalk';
import { OpenAI } from 'openai';
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
import { removeContentTags } from '../utils/removeContentTags';
import { tokenCount } from '../utils/tokenCount';
import { AiEngine, AiEngineConfig } from './Engine';
@@ -54,8 +55,8 @@ export class AnthropicEngine implements AiEngine {
const data = await this.client.messages.create(params);
const message = data?.content[0].text;
return message;
let content = message;
return removeContentTags(content, 'think');
} catch (error) {
const err = error as Error;
outro(`${chalk.red('✖')} ${err?.message || err}`);

View File

@@ -7,6 +7,7 @@ import axios from 'axios';
import chalk from 'chalk';
import { OpenAI } from 'openai';
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
import { removeContentTags } from '../utils/removeContentTags';
import { tokenCount } from '../utils/tokenCount';
import { AiEngine, AiEngineConfig } from './Engine';
@@ -52,7 +53,9 @@ export class AzureEngine implements AiEngine {
if (message?.content === null) {
return undefined;
}
return message?.content;
let content = message?.content;
return removeContentTags(content, 'think');
} catch (error) {
outro(`${chalk.red('✖')} ${this.config.model}`);

61
src/engine/deepseek.ts Normal file
View File

@@ -0,0 +1,61 @@
import axios from 'axios';
import { OpenAI } from 'openai';
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
import { removeContentTags } from '../utils/removeContentTags';
import { tokenCount } from '../utils/tokenCount';
import { OpenAiEngine, OpenAiConfig } from './openAi';
export interface DeepseekConfig extends OpenAiConfig {}
export class DeepseekEngine extends OpenAiEngine {
constructor(config: DeepseekConfig) {
// Call OpenAIEngine constructor with forced Deepseek baseURL
super({
...config,
baseURL: 'https://api.deepseek.com/v1'
});
}
// Identical method from OpenAiEngine, re-implemented here
public generateCommitMessage = async (
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
): Promise<string | null> => {
const params = {
model: this.config.model,
messages,
temperature: 0,
top_p: 0.1,
max_tokens: this.config.maxTokensOutput
};
try {
const REQUEST_TOKENS = messages
.map((msg) => tokenCount(msg.content as string) + 4)
.reduce((a, b) => a + b, 0);
if (
REQUEST_TOKENS >
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;
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 openAiError = error.response.data.error;
if (openAiError) throw new Error(openAiError.message);
}
throw err;
}
};
}

View File

@@ -1,5 +1,6 @@
import axios, { AxiosInstance } from 'axios';
import { OpenAI } from 'openai';
import { removeContentTags } from '../utils/removeContentTags';
import { AiEngine, AiEngineConfig } from './Engine';
interface FlowiseAiConfig extends AiEngineConfig {}
@@ -36,7 +37,8 @@ export class FlowiseEngine implements AiEngine {
try {
const response = await this.client.post('', payload);
const message = response.data;
return message?.text;
let content = message?.text;
return removeContentTags(content, 'think');
} catch (err: any) {
const message = err.response?.data?.error ?? err.message;
throw new Error('local model issues. details: ' + message);

View File

@@ -7,6 +7,7 @@ import {
} from '@google/generative-ai';
import axios from 'axios';
import { OpenAI } from 'openai';
import { removeContentTags } from '../utils/removeContentTags';
import { AiEngine, AiEngineConfig } from './Engine';
interface GeminiConfig extends AiEngineConfig {}
@@ -71,7 +72,8 @@ export class GeminiEngine implements AiEngine {
}
});
return result.response.text();
const content = result.response.text();
return removeContentTags(content, 'think');
} catch (error) {
const err = error as Error;
if (

10
src/engine/groq.ts Normal file
View File

@@ -0,0 +1,10 @@
import { OpenAiConfig, OpenAiEngine } from './openAi';
interface GroqConfig extends OpenAiConfig {}
export class GroqEngine extends OpenAiEngine {
constructor(config: GroqConfig) {
config.baseURL = 'https://api.groq.com/openai/v1';
super(config);
}
}

79
src/engine/mistral.ts Normal file
View File

@@ -0,0 +1,79 @@
import axios from 'axios';
import { OpenAI } from 'openai';
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
import { removeContentTags } from '../utils/removeContentTags';
import { tokenCount } from '../utils/tokenCount';
import { AiEngine, AiEngineConfig } from './Engine';
// Using any for Mistral types to avoid type declaration issues
export interface MistralAiConfig extends AiEngineConfig {}
export type MistralCompletionMessageParam = Array<any>;
// Import Mistral dynamically to avoid TS errors
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Mistral = require('@mistralai/mistralai').Mistral;
export class MistralAiEngine implements AiEngine {
config: MistralAiConfig;
client: any; // Using any type for Mistral client to avoid TS errors
constructor(config: MistralAiConfig) {
this.config = config;
if (!config.baseURL) {
this.client = new Mistral({ apiKey: config.apiKey });
} else {
this.client = new Mistral({
apiKey: config.apiKey,
serverURL: config.baseURL
});
}
}
public generateCommitMessage = async (
messages: Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>
): Promise<string | null> => {
const params = {
model: this.config.model,
messages: messages as MistralCompletionMessageParam,
topP: 0.1,
maxTokens: this.config.maxTokensOutput
};
try {
const REQUEST_TOKENS = messages
.map((msg) => tokenCount(msg.content as string) + 4)
.reduce((a, b) => a + b, 0);
if (
REQUEST_TOKENS >
this.config.maxTokensInput - this.config.maxTokensOutput
)
throw new Error(GenerateCommitMessageErrorEnum.tooMuchTokens);
const completion = await this.client.chat.complete(params);
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.');
let content = message.content as string;
return removeContentTags(content, 'think');
} catch (error) {
const err = error as Error;
if (
axios.isAxiosError<{ error?: { message: string } }>(error) &&
error.response?.status === 401
) {
const mistralError = error.response.data.error;
if (mistralError) throw new Error(mistralError.message);
}
throw err;
}
};
}

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

@@ -0,0 +1,47 @@
import axios, { AxiosInstance } from 'axios';
import { OpenAI } from 'openai';
import { removeContentTags } from '../utils/removeContentTags';
import { AiEngine, AiEngineConfig } from './Engine';
interface MLXConfig extends AiEngineConfig {}
export class MLXEngine implements AiEngine {
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' }
});
}
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

@@ -1,5 +1,6 @@
import axios, { AxiosInstance } from 'axios';
import { OpenAI } from 'openai';
import { removeContentTags } from '../utils/removeContentTags';
import { AiEngine, AiEngineConfig } from './Engine';
interface OllamaConfig extends AiEngineConfig {}
@@ -10,11 +11,18 @@ export class OllamaEngine implements AiEngine {
constructor(config) {
this.config = config;
// Combine base headers with custom headers
const headers = {
'Content-Type': 'application/json',
...config.customHeaders
};
this.client = axios.create({
url: config.baseURL
? `${config.baseURL}/${config.apiKey}`
: 'http://localhost:11434/api/chat',
headers: { 'Content-Type': 'application/json' }
headers
});
}
@@ -33,9 +41,9 @@ export class OllamaEngine implements AiEngine {
params
);
const message = response.data.message;
return message?.content;
const { message } = response.data;
let content = message?.content;
return removeContentTags(content, 'think');
} catch (err: any) {
const message = err.response?.data?.error ?? err.message;
throw new Error(`Ollama provider error: ${message}`);

View File

@@ -1,10 +1,12 @@
import axios from 'axios';
import { OpenAI } from 'openai';
import { GenerateCommitMessageErrorEnum } from '../generateCommitMessageFromGitDiff';
import { parseCustomHeaders } from '../utils/engine';
import { removeContentTags } from '../utils/removeContentTags';
import { tokenCount } from '../utils/tokenCount';
import { AiEngine, AiEngineConfig } from './Engine';
interface OpenAiConfig extends AiEngineConfig {}
export interface OpenAiConfig extends AiEngineConfig {}
export class OpenAiEngine implements AiEngine {
config: OpenAiConfig;
@@ -12,7 +14,23 @@ export class OpenAiEngine implements AiEngine {
constructor(config: OpenAiConfig) {
this.config = config;
this.client = new OpenAI({ apiKey: config.apiKey });
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);
}
public generateCommitMessage = async (
@@ -40,8 +58,8 @@ export class OpenAiEngine implements AiEngine {
const completion = await this.client.chat.completions.create(params);
const message = completion.choices[0].message;
return message?.content;
let content = message?.content;
return removeContentTags(content, 'think');
} catch (error) {
const err = error as Error;
if (

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

@@ -11,9 +11,13 @@ const MAX_TOKENS_OUTPUT = config.OCO_TOKENS_MAX_OUTPUT;
const generateCommitMessageChatCompletionPrompt = async (
diff: string,
fullGitMojiSpec: boolean
fullGitMojiSpec: boolean,
context: string
): Promise<Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>> => {
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(fullGitMojiSpec);
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(
fullGitMojiSpec,
context
);
const chatContextAsCompletionRequest = [...INIT_MESSAGES_PROMPT];
@@ -36,10 +40,14 @@ const ADJUSTMENT_FACTOR = 20;
export const generateCommitMessageByDiff = async (
diff: string,
fullGitMojiSpec: boolean = false
fullGitMojiSpec: boolean = false,
context: string = ''
): Promise<string> => {
try {
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(fullGitMojiSpec);
const INIT_MESSAGES_PROMPT = await getMainCommitPrompt(
fullGitMojiSpec,
context
);
const INIT_MESSAGES_PROMPT_LENGTH = INIT_MESSAGES_PROMPT.map(
(msg) => tokenCount(msg.content as string) + 4
@@ -69,7 +77,8 @@ export const generateCommitMessageByDiff = async (
const messages = await generateCommitMessageChatCompletionPrompt(
diff,
fullGitMojiSpec
fullGitMojiSpec,
context
);
const engine = getEngine();

View File

@@ -2,5 +2,7 @@
"localLanguage": "česky",
"commitFix": "fix(server.ts): zlepšení velikosti proměnné port na velká písmena PORT",
"commitFeat": "feat(server.ts): přidání podpory pro proměnnou prostředí process.env.PORT",
"commitDescription": "Proměnná port se nyní jmenuje PORT, což odpovídá konvenci pojmenování, protože PORT je konstanta. Podpora proměnné prostředí process.env.PORT umožňuje snadnější správu nastavení při spuštění."
"commitDescription": "Proměnná port se nyní jmenuje PORT, což odpovídá konvenci pojmenování, protože PORT je konstanta. Podpora proměnné prostředí process.env.PORT umožňuje snadnější správu nastavení při spuštění.",
"commitFixOmitScope": "fix: zlepšení velikosti proměnné port na velká písmena PORT",
"commitFeatOmitScope": "feat: přidání podpory pro proměnnou prostředí process.env.PORT"
}

View File

@@ -2,5 +2,7 @@
"localLanguage": "Deutsch",
"commitFix": "fix(server.ts): Ändere die Groß- und Kleinschreibung der Port-Variable von Kleinbuchstaben auf Großbuchstaben PORT.",
"commitFeat": "Funktion(server.ts): Unterstützung für die Umgebungsvariable process.env.PORT hinzufügen",
"commitDescription": "Die Port-Variable heißt jetzt PORT, was die Konsistenz mit den Namenskonventionen verbessert, da PORT eine Konstante ist. Die Unterstützung für eine Umgebungsvariable ermöglicht es der Anwendung, flexibler zu sein, da sie jetzt auf jedem verfügbaren Port laufen kann, der über die Umgebungsvariable process.env.PORT angegeben wird."
"commitDescription": "Die Port-Variable heißt jetzt PORT, was die Konsistenz mit den Namenskonventionen verbessert, da PORT eine Konstante ist. Die Unterstützung für eine Umgebungsvariable ermöglicht es der Anwendung, flexibler zu sein, da sie jetzt auf jedem verfügbaren Port laufen kann, der über die Umgebungsvariable process.env.PORT angegeben wird.",
"commitFixOmitScope": "fix: Ändere die Groß- und Kleinschreibung der Port-Variable von Kleinbuchstaben auf Großbuchstaben PORT.",
"commitFeatOmitScope": "Funktion: Unterstützung für die Umgebungsvariable process.env.PORT hinzufügen"
}

View File

@@ -2,5 +2,7 @@
"localLanguage": "english",
"commitFix": "fix(server.ts): change port variable case from lowercase port to uppercase PORT to improve semantics",
"commitFeat": "feat(server.ts): add support for process.env.PORT environment variable to be able to run app on a configurable port",
"commitDescription": "The port variable is now named PORT, which improves consistency with the naming conventions as PORT is a constant. Support for an environment variable allows the application to be more flexible as it can now run on any available port specified via the process.env.PORT environment variable."
"commitDescription": "The port variable is now named PORT, which improves consistency with the naming conventions as PORT is a constant. Support for an environment variable allows the application to be more flexible as it can now run on any available port specified via the process.env.PORT environment variable.",
"commitFixOmitScope": "fix: change port variable case from lowercase port to uppercase PORT to improve semantics",
"commitFeatOmitScope": "feat: add support for process.env.PORT environment variable to be able to run app on a configurable port"
}

View File

@@ -2,5 +2,7 @@
"localLanguage": "spanish",
"commitFix": "fix(server.ts): cambiar la variable port de minúsculas a mayúsculas PORT",
"commitFeat": "feat(server.ts): añadir soporte para la variable de entorno process.env.PORT",
"commitDescription": "La variable port ahora se llama PORT, lo que mejora la coherencia con las convenciones de nomenclatura, ya que PORT es una constante. El soporte para una variable de entorno permite que la aplicación sea más flexible, ya que ahora puede ejecutarse en cualquier puerto disponible especificado a través de la variable de entorno process.env.PORT."
"commitDescription": "La variable port ahora se llama PORT, lo que mejora la coherencia con las convenciones de nomenclatura, ya que PORT es una constante. El soporte para una variable de entorno permite que la aplicación sea más flexible, ya que ahora puede ejecutarse en cualquier puerto disponible especificado a través de la variable de entorno process.env.PORT.",
"commitFixOmitScope": "fix: cambiar la variable port de minúsculas a mayúsculas PORT",
"commitFeatOmitScope": "feat: añadir soporte para la variable de entorno process.env.PORT"
}

View File

@@ -2,5 +2,7 @@
"localLanguage": "française",
"commitFix": "corriger(server.ts) : changer la casse de la variable de port de minuscules à majuscules (PORT)",
"commitFeat": "fonctionnalité(server.ts) : ajouter la prise en charge de la variable d'environnement process.env.PORT",
"commitDescription": "La variable de port est maintenant nommée PORT, ce qui améliore la cohérence avec les conventions de nommage car PORT est une constante. La prise en charge d'une variable d'environnement permet à l'application d'être plus flexible car elle peut maintenant s'exécuter sur n'importe quel port disponible spécifié via la variable d'environnement process.env.PORT."
"commitDescription": "La variable de port est maintenant nommée PORT, ce qui améliore la cohérence avec les conventions de nommage car PORT est une constante. La prise en charge d'une variable d'environnement permet à l'application d'être plus flexible car elle peut maintenant s'exécuter sur n'importe quel port disponible spécifié via la variable d'environnement process.env.PORT.",
"commitFixOmitScope": "corriger : changer la casse de la variable de port de minuscules à majuscules (PORT)",
"commitFeatOmitScope": "fonctionnalité : ajouter la prise en charge de la variable d'environnement process.env.PORT"
}

View File

@@ -2,5 +2,7 @@
"localLanguage": "bahasa",
"commitFix": "fix(server.ts): mengubah huruf port variable dari huruf kecil ke huruf besar PORT",
"commitFeat": "feat(server.ts): menambahkan support di process.env.PORT environment variabel",
"commitDescription": "Port variabel bernama PORT, yang membantu konsistensi dengan memberi nama yaitu PORT yang konstan. Bantuan environment variabel membantu aplikasi lebih fleksibel, dan dapat di jalankan di port manapun yang tertulis pada process.env.PORT"
"commitDescription": "Port variabel bernama PORT, yang membantu konsistensi dengan memberi nama yaitu PORT yang konstan. Bantuan environment variabel membantu aplikasi lebih fleksibel, dan dapat di jalankan di port manapun yang tertulis pada process.env.PORT",
"commitFixOmitScope": "fix: mengubah huruf port variable dari huruf kecil ke huruf besar PORT",
"commitFeatOmitScope": "feat: menambahkan support di process.env.PORT environment variabel"
}

View File

@@ -2,5 +2,7 @@
"localLanguage": "italiano",
"commitFix": "fix(server.ts): cambia la grafia della variabile della porta dal minuscolo port al maiuscolo PORT",
"commitFeat": "feat(server.ts): aggiunge il supporto per la variabile di ambiente process.env.PORT",
"commitDescription": "La variabile port è ora chiamata PORT, migliorando la coerenza con le convenzioni di denominazione in quanto PORT è una costante. Il supporto per una variabile di ambiente consente all'applicazione di essere più flessibile poiché ora può essere eseguita su qualsiasi porta disponibile specificata tramite la variabile di ambiente process.env.PORT."
"commitDescription": "La variabile port è ora chiamata PORT, migliorando la coerenza con le convenzioni di denominazione in quanto PORT è una costante. Il supporto per una variabile di ambiente consente all'applicazione di essere più flessibile poiché ora può essere eseguita su qualsiasi porta disponibile specificata tramite la variabile di ambiente process.env.PORT.",
"commitFixOmitScope": "fix: cambia la grafia della variabile della porta dal minuscolo port al maiuscolo PORT",
"commitFeatOmitScope": "feat: aggiunge il supporto per la variabile di ambiente process.env.PORT"
}

View File

@@ -2,5 +2,7 @@
"localLanguage": "日本語",
"commitFix": "修正(server.ts): ポート変数を小文字のportから大文字のPORTに変更",
"commitFeat": "新機能(server.ts): 環境変数process.env.PORTのサポートを追加",
"commitDescription": "ポート変数は現在PORTという名前になり、定数であるPORTを使うことで命名規則に一貫性が生まれました。環境変数をサポートすることで、環境変数process.env.PORTで指定された任意の利用可能なポートで実行できるようになり、アプリケーションはより柔軟になりました。"
"commitDescription": "ポート変数は現在PORTという名前になり、定数であるPORTを使うことで命名規則に一貫性が生まれました。環境変数をサポートすることで、環境変数process.env.PORTで指定された任意の利用可能なポートで実行できるようになり、アプリケーションはより柔軟になりました。",
"commitFixOmitScope": "修正: ポート変数を小文字のportから大文字のPORTに変更",
"commitFeatOmitScope": "新機能: 環境変数process.env.PORTのサポートを追加"
}

View File

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

View File

@@ -2,5 +2,7 @@
"localLanguage": "Nederlands",
"commitFix": "fix(server.ts): verander poortvariabele van kleine letters poort naar hoofdletters PORT",
"commitFeat": "feat(server.ts): voeg ondersteuning toe voor process.env.PORT omgevingsvariabele",
"commitDescription": "De poortvariabele heet nu PORT, wat de consistentie met de naamgevingsconventies verbetert omdat PORT een constante is. Ondersteuning voor een omgevingsvariabele maakt de applicatie flexibeler, omdat deze nu kan draaien op elke beschikbare poort die is gespecificeerd via de process.env.PORT omgevingsvariabele."
"commitDescription": "De poortvariabele heet nu PORT, wat de consistentie met de naamgevingsconventies verbetert omdat PORT een constante is. Ondersteuning voor een omgevingsvariabele maakt de applicatie flexibeler, omdat deze nu kan draaien op elke beschikbare poort die is gespecificeerd via de process.env.PORT omgevingsvariabele.",
"commitFixOmitScope": "fix: verander poortvariabele van kleine letters poort naar hoofdletters PORT",
"commitFeatOmitScope": "feat: voeg ondersteuning toe voor process.env.PORT omgevingsvariabele"
}

View File

@@ -2,5 +2,7 @@
"localLanguage": "polski",
"commitFix": "fix(server.ts): poprawa wielkości zmiennej port na pisane z dużymi literami PORT",
"commitFeat": "feat(server.ts): dodanie obsługi zmiennej środowiskowej process.env.PORT",
"commitDescription": "Zmienna port jest teraz nazwana PORT, co jest zgodne z konwencją nazewniczą ponieważ PORT jest stałą. Obsługa zmiennej środowiskowej process.env.PORT pozwala łatwiej zarządzać ustawieniami przy starcie."
"commitDescription": "Zmienna port jest teraz nazwana PORT, co jest zgodne z konwencją nazewniczą ponieważ PORT jest stałą. Obsługa zmiennej środowiskowej process.env.PORT pozwala łatwiej zarządzać ustawieniami przy starcie.",
"commitFixOmitScope": "fix: poprawa wielkości zmiennej port na pisane z dużymi literami PORT",
"commitFeatOmitScope": "feat: dodanie obsługi zmiennej środowiskowej process.env.PORT"
}

View File

@@ -2,5 +2,7 @@
"localLanguage": "português",
"commitFix": "fix(server.ts): altera o caso da variável de porta de port minúscula para PORT maiúscula",
"commitFeat": "feat(server.ts): adiciona suporte para a variável de ambiente process.env.PORT",
"commitDescription": "A variável de porta agora é denominada PORT, o que melhora a consistência com as convenções de nomenclatura, pois PORT é uma constante. O suporte para uma variável de ambiente permite que o aplicativo seja mais flexível, pois agora pode ser executado em qualquer porta disponível especificada por meio da variável de ambiente process.env.PORT."
"commitDescription": "A variável de porta agora é denominada PORT, o que melhora a consistência com as convenções de nomenclatura, pois PORT é uma constante. O suporte para uma variável de ambiente permite que o aplicativo seja mais flexível, pois agora pode ser executado em qualquer porta disponível especificada por meio da variável de ambiente process.env.PORT.",
"commitFixOmitScope": "fix: altera o caso da variável de porta de port minúscula para PORT maiúscula",
"commitFeatOmitScope": "feat: adiciona suporte para a variável de ambiente process.env.PORT"
}

View File

@@ -2,5 +2,7 @@
"localLanguage": "русский",
"commitFix": "fix(server.ts): изменение регистра переменной порта с нижнего регистра port на верхний регистр PORT",
"commitFeat": "feat(server.ts): добавлена поддержка переменной окружения process.env.PORT",
"commitDescription": "Переменная port теперь называется PORT, что улучшает согласованность с соглашениями об именовании констант. Поддержка переменной окружения позволяет приложению быть более гибким, запускаясь на любом доступном порту, указанном с помощью переменной окружения process.env.PORT."
"commitDescription": "Переменная port теперь называется PORT, что улучшает согласованность с соглашениями об именовании констант. Поддержка переменной окружения позволяет приложению быть более гибким, запускаясь на любом доступном порту, указанном с помощью переменной окружения process.env.PORT.",
"commitFixOmitScope": "fix: изменение регистра переменной порта с нижнего регистра port на верхний регистр PORT",
"commitFeatOmitScope": "feat: добавлена поддержка переменной окружения process.env.PORT"
}

View File

@@ -2,5 +2,7 @@
"localLanguage": "svenska",
"commitFix": "fixa(server.ts): ändra variabelnamnet för port från små bokstäver till stora bokstäver PORT",
"commitFeat": "nyhet(server.ts): lägg till stöd för process.env.PORT miljövariabel",
"commitDescription": "Variabeln som innehåller portnumret heter nu PORT vilket förbättrar konsekvensen med namngivningskonventionerna eftersom PORT är en konstant. Stöd för en miljövariabel gör att applikationen kan vara mer flexibel då den nu kan köras på vilken port som helst som specificeras via miljövariabeln process.env.PORT."
"commitDescription": "Variabeln som innehåller portnumret heter nu PORT vilket förbättrar konsekvensen med namngivningskonventionerna eftersom PORT är en konstant. Stöd för en miljövariabel gör att applikationen kan vara mer flexibel då den nu kan köras på vilken port som helst som specificeras via miljövariabeln process.env.PORT.",
"commitFixOmitScope": "fixa: ändra variabelnamnet för port från små bokstäver till stora bokstäver PORT",
"commitFeatOmitScope": "nyhet: lägg till stöd för process.env.PORT miljövariabel"
}

View File

@@ -2,5 +2,7 @@
"localLanguage": "ไทย",
"commitFix": "fix(server.ts): เปลี่ยนตัวพิมพ์ของตัวแปร จากตัวพิมพ์เล็ก port เป็นตัวพิมพ์ใหญ่ PORT",
"commitFeat": "feat(server.ts): เพิ่มการรองรับสำหรับตัวแปรสภาพแวดล้อม process.env.PORT",
"commitDescription": "ตอนนี้ตัวแปรพอร์ตมีชื่อว่า PORT, ซึ่งปรับปรุงความสอดคล้องกับหลักการตั้งชื่อเนื่องจาก PORT เป็นค่าคงที่. การสนับสนุนสำหรับตัวแปรสภาพแวดล้อม ช่วยให้แอปพลิเคชันมีความยืดหยุ่นมากขึ้นเนื่องจาก สามารถทำงานบนพอร์ตใด ๆ ตามที่กำหนด ซึ่งระบุผ่านตัวแปรสภาพแวดล้อม process.env.PORT"
"commitDescription": "ตอนนี้ตัวแปรพอร์ตมีชื่อว่า PORT, ซึ่งปรับปรุงความสอดคล้องกับหลักการตั้งชื่อเนื่องจาก PORT เป็นค่าคงที่. การสนับสนุนสำหรับตัวแปรสภาพแวดล้อม ช่วยให้แอปพลิเคชันมีความยืดหยุ่นมากขึ้นเนื่องจาก สามารถทำงานบนพอร์ตใด ๆ ตามที่กำหนด ซึ่งระบุผ่านตัวแปรสภาพแวดล้อม process.env.PORT",
"commitFixOmitScope": "fix: เปลี่ยนตัวพิมพ์ของตัวแปร จากตัวพิมพ์เล็ก port เป็นตัวพิมพ์ใหญ่ PORT",
"commitFeatOmitScope": "feat: เพิ่มการรองรับสำหรับตัวแปรสภาพแวดล้อม process.env.PORT"
}

View File

@@ -2,5 +2,7 @@
"localLanguage": "Turkish",
"commitFix": "fix(server.ts): port değişkeni küçük harfli porttan büyük harfli PORT'a değiştirildi",
"commitFeat": "feat(server.ts): process.env.PORT ortam değişkeni için destek eklendi.",
"commitDescription": "Bağlantı noktası değişkeni artık PORT olarak adlandırıldı ve PORT bir sabit değişken olduğu için bu adlandırma tutarlılığı artırır. Ortam değişkeni desteği, artık process.env.PORT ortam değişkeni aracılığıyla belirtilen herhangi bir kullanılabilir bağlantı noktasında çalışabileceğinden uygulamanın daha esnek olmasını sağlar."
"commitDescription": "Bağlantı noktası değişkeni artık PORT olarak adlandırıldı ve PORT bir sabit değişken olduğu için bu adlandırma tutarlılığı artırır. Ortam değişkeni desteği, artık process.env.PORT ortam değişkeni aracılığıyla belirtilen herhangi bir kullanılabilir bağlantı noktasında çalışabileceğinden uygulamanın daha esnek olmasını sağlar.",
"commitFixOmitScope": "fix: port değişkeni küçük harfli porttan büyük harfli PORT'a değiştirildi",
"commitFeatOmitScope": "feat: process.env.PORT ortam değişkeni için destek eklendi."
}

View File

@@ -2,5 +2,7 @@
"localLanguage": "vietnamese",
"commitFix": "fix(server.ts): thay đổi chữ viết thường của biến port thành chữ viết hoa PORT",
"commitFeat": "feat(server.ts): thêm hỗ trợ cho biến môi trường process.env.PORT",
"commitDescription": "Biến port đã được đổi tên thành PORT, giúp cải thiện tính nhất quán trong việc đặt tên theo quy ước vì PORT là một hằng số. Hỗ trợ cho biến môi trường cho phép ứng dụng linh hoạt hơn khi có thể chạy trên bất kỳ cổng nào được chỉ định thông qua biến môi trường process.env.PORT."
"commitDescription": "Biến port đã được đổi tên thành PORT, giúp cải thiện tính nhất quán trong việc đặt tên theo quy ước vì PORT là một hằng số. Hỗ trợ cho biến môi trường cho phép ứng dụng linh hoạt hơn khi có thể chạy trên bất kỳ cổng nào được chỉ định thông qua biến môi trường process.env.PORT.",
"commitFixOmitScope": "fix: thay đổi chữ viết thường của biến port thành chữ viết hoa PORT",
"commitFeatOmitScope": "feat: thêm hỗ trợ cho biến môi trường process.env.PORT"
}

View File

@@ -2,5 +2,7 @@
"localLanguage": "简体中文",
"commitFix": "fix(server.ts)将端口变量从小写port改为大写PORT",
"commitFeat": "feat(server.ts)添加对process.env.PORT环境变量的支持",
"commitDescription": "现在端口变量被命名为PORT这提高了命名约定的一致性因为PORT是一个常量。环境变量的支持使应用程序更加灵活因为它现在可以通过process.env.PORT环境变量在任何可用端口上运行。"
"commitDescription": "现在端口变量被命名为PORT这提高了命名约定的一致性因为PORT是一个常量。环境变量的支持使应用程序更加灵活因为它现在可以通过process.env.PORT环境变量在任何可用端口上运行。",
"commitFixOmitScope": "fix将端口变量从小写port改为大写PORT",
"commitFeatOmitScope": "feat添加对process.env.PORT环境变量的支持"
}

View File

@@ -2,5 +2,7 @@
"localLanguage": "繁體中文",
"commitFix": "修正(server.ts)將端口變數從小寫端口改為大寫PORT",
"commitFeat": "功能(server.ts)新增對process.env.PORT環境變數的支援",
"commitDescription": "現在port變數已更名為PORT以符合命名慣例因為PORT是一個常量。支援環境變數可以使應用程序更靈活因為它現在可以通過process.env.PORT環境變數運行在任何可用端口上。"
"commitDescription": "現在port變數已更名為PORT以符合命名慣例因為PORT是一個常量。支援環境變數可以使應用程序更靈活因為它現在可以通過process.env.PORT環境變數運行在任何可用端口上。",
"commitFixOmitScope": "修正將端口變數從小寫端口改為大寫PORT",
"commitFeatOmitScope": "功能新增對process.env.PORT環境變數的支援"
}

View File

@@ -10,10 +10,12 @@ export default function () {
const entriesToSet: [key: string, value: string | boolean | number][] = [];
for (const entry of Object.entries(DEFAULT_CONFIG)) {
const [key, _value] = entry;
if (config[key] === 'undefined') entriesToSet.push(entry);
if (config[key] === 'undefined' || config[key] === undefined)
entriesToSet.push(entry);
}
if (entriesToSet.length > 0) setConfig(entriesToSet);
console.log(entriesToSet);
};
setDefaultConfigValues(getGlobalConfig());

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;
@@ -53,6 +66,7 @@ export const runMigrations = async () => {
migration.name
}: ${error}`
);
process.exit(1);
}
isMigrated = true;

View File

@@ -53,7 +53,7 @@ export const configureCommitlintIntegration = async (force = false) => {
spin.start('Generating consistency with given @commitlint rules');
const prompts = inferPromptsFromCommitlintConfig(commitLintConfig);
const prompts = inferPromptsFromCommitlintConfig(commitLintConfig as any);
const consistencyPrompts =
commitlintPrompts.GEN_COMMITLINT_CONSISTENCY_PROMPT(prompts);

View File

@@ -58,16 +58,16 @@ const llmReadableRules: {
caseRule: (key, applicable, value: string | Array<string>) =>
`The ${key} should ${applicable} be in ${
Array.isArray(value)
? `one of the following case:
? `one of the following case:
- ${value.join('\n - ')}.`
: `${value} case.`
}`,
emptyRule: (key, applicable) => `The ${key} should ${applicable} be empty.`,
enumRule: (key, applicable, value: string | Array<string>) =>
`The ${key} should ${applicable} be one of the following values:
`The ${key} should ${applicable} be one of the following values:
- ${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:
`The ${key} should ${applicable} be one of the following values:
- ${
Array.isArray(value)
? value
@@ -204,7 +204,11 @@ export const inferPromptsFromCommitlintConfig = (
* ubiquitous language from @commitlint.
* While gpt-4 does this on it self, gpt-3.5 can't map this on his own atm.
*/
const STRUCTURE_OF_COMMIT = `
const STRUCTURE_OF_COMMIT = config.OCO_OMIT_SCOPE
? `
- Header of commit is composed of type and subject: <type-of-commit>: <subject-of-commit>
- Description of commit is composed of body and footer (optional): <body-of-commit>\n<footer(s)-of-commit>`
: `
- Header of commit is composed of type, scope, subject: <type-of-commit>(<scope-of-commit>): <subject-of-commit>
- Description of commit is composed of body and footer (optional): <body-of-commit>\n<footer(s)-of-commit>`;
@@ -220,8 +224,12 @@ Here are the specific requirements and conventions that should be strictly follo
Commit Message Conventions:
- The commit message consists of three parts: Header, Body, and Footer.
- Header:
- Format: \`<type>(<scope>): <subject>\`
- Header:
- Format: ${
config.OCO_OMIT_SCOPE
? '`<type>: <subject>`'
: '`<type>(<scope>): <subject>`'
}
- ${prompts.join('\n- ')}
JSON Output Format:
@@ -229,17 +237,19 @@ JSON Output Format:
\`\`\`json
{
"localLanguage": "${translation.localLanguage}",
"commitFix": "<Header of commit for bug fix>",
"commitFeat": "<Header of commit for feature>",
"commitFix": "<Header of commit for bug fix with scope>",
"commitFeat": "<Header of commit for feature with scope>",
"commitFixOmitScope": "<Header of commit for bug fix without scope>",
"commitFeatOmitScope": "<Header of commit for feature without scope>",
"commitDescription": "<Description of commit for both the bug fix and the feature>"
}
\`\`\`
- The "commitDescription" should not include the commit messages header, only the description.
- The "commitDescription" should not include the commit message's header, only the description.
- Description should not be more than 74 characters.
Additional Details:
- Changing the variable 'port' to uppercase 'PORT' is considered a bug fix.
- Allowing the server to listen on a port specified through the environment variable is considered a new feature.
- Changing the variable 'port' to uppercase 'PORT' is considered a bug fix.
- Allowing the server to listen on a port specified through the environment variable is considered a new feature.
Example Git Diff is to follow:`
},
@@ -277,7 +287,11 @@ ${
? '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

@@ -4,7 +4,8 @@ import path from 'path';
const findModulePath = (moduleName: string) => {
const searchPaths = [
path.join('node_modules', moduleName),
path.join('node_modules', '.pnpm')
path.join('node_modules', '.pnpm'),
path.resolve(__dirname, '../..')
];
for (const basePath of searchPaths) {
@@ -59,7 +60,7 @@ export const getCommitLintPWDConfig =
* ES Module (commitlint@v19.x.x. <= )
* Directory import is not supported in ES Module resolution, so import the file directly
*/
modulePath = await findModulePath('@commitlint/load/lib/load.js');
modulePath = findModulePath('@commitlint/load/lib/load.js');
load = (await import(modulePath)).default;
break;
}

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

@@ -108,12 +108,32 @@ const getDescriptionInstruction = () =>
const getOneLineCommitInstruction = () =>
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.'
? 'Craft a concise, single sentence, 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 one single message.'
: '';
const getScopeInstruction = () =>
config.OCO_OMIT_SCOPE
? 'Do not include a scope in the commit message format. Use the format: <type>: <subject>'
: '';
/**
* Get the context of the user input
* @param extraArgs - The arguments passed to the command line
* @example
* $ oco -- This is a context used to generate the commit message
* @returns - The context of the user input
*/
const userInputCodeContext = (context: string) => {
if (context !== '' && context !== ' ') {
return `Additional context provided by the user: <context>${context}</context>\nConsider this context when generating the commit message, incorporating relevant information when appropriate.`;
}
return '';
};
const INIT_MAIN_PROMPT = (
language: string,
fullGitMojiSpec: boolean
fullGitMojiSpec: boolean,
context: string
): OpenAI.Chat.Completions.ChatCompletionMessageParam => ({
role: 'system',
content: (() => {
@@ -126,9 +146,11 @@ const INIT_MAIN_PROMPT = (
const conventionGuidelines = getCommitConvention(fullGitMojiSpec);
const descriptionGuideline = getDescriptionInstruction();
const oneLineCommitGuideline = getOneLineCommitInstruction();
const scopeInstruction = getScopeInstruction();
const generalGuidelines = `Use the present tense. Lines must not be longer than 74 characters. Use ${language} for the commit message.`;
const userInputContext = userInputCodeContext(context);
return `${missionStatement}\n${diffInstruction}\n${conventionGuidelines}\n${descriptionGuideline}\n${oneLineCommitGuideline}\n${generalGuidelines}`;
return `${missionStatement}\n${diffInstruction}\n${conventionGuidelines}\n${descriptionGuideline}\n${oneLineCommitGuideline}\n${scopeInstruction}\n${generalGuidelines}\n${userInputContext}`;
})()
});
@@ -161,31 +183,52 @@ export const INIT_DIFF_PROMPT: OpenAI.Chat.Completions.ChatCompletionMessagePara
});`
};
const getContent = (translation: ConsistencyPrompt) => {
const fix = config.OCO_EMOJI
? `🐛 ${removeConventionalCommitWord(translation.commitFix)}`
: translation.commitFix;
const COMMIT_TYPES = {
fix: '🐛',
feat: '✨'
} as const;
const feat = config.OCO_EMOJI
? `${removeConventionalCommitWord(translation.commitFeat)}`
: translation.commitFeat;
const generateCommitString = (
type: keyof typeof COMMIT_TYPES,
message: string
): string => {
const cleanMessage = removeConventionalCommitWord(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 featMessage =
config.OCO_OMIT_SCOPE && translation.commitFeatOmitScope
? translation.commitFeatOmitScope
: translation.commitFeat;
const fix = generateCommitString('fix', fixMessage);
const feat = config.OCO_ONE_LINE_COMMIT
? ''
: generateCommitString('feat', featMessage);
const description = config.OCO_DESCRIPTION
? translation.commitDescription
: '';
return `${fix}\n${feat}\n${description}`;
return [fix, feat, description].filter(Boolean).join('\n');
};
const INIT_CONSISTENCY_PROMPT = (
translation: ConsistencyPrompt
): OpenAI.Chat.Completions.ChatCompletionMessageParam => ({
role: 'assistant',
content: getContent(translation)
content: getConsistencyContent(translation)
});
export const getMainCommitPrompt = async (
fullGitMojiSpec: boolean
fullGitMojiSpec: boolean,
context: string
): Promise<Array<OpenAI.Chat.Completions.ChatCompletionMessageParam>> => {
switch (config.OCO_PROMPT_MODULE) {
case '@commitlint':
@@ -214,7 +257,7 @@ export const getMainCommitPrompt = async (
default:
return [
INIT_MAIN_PROMPT(translation.localLanguage, fullGitMojiSpec),
INIT_MAIN_PROMPT(translation.localLanguage, fullGitMojiSpec, context),
INIT_DIFF_PROMPT,
INIT_CONSISTENCY_PROMPT(translation)
];

View File

@@ -6,18 +6,49 @@ import { FlowiseEngine } from '../engine/flowise';
import { GeminiEngine } from '../engine/gemini';
import { OllamaEngine } from '../engine/ollama';
import { OpenAiEngine } from '../engine/openAi';
import { MistralAiEngine } from '../engine/mistral';
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;
} else {
parsedHeaders = JSON.parse(headers);
}
} catch (error) {
console.warn(
'Invalid OCO_API_CUSTOM_HEADERS format, ignoring custom headers'
);
}
return parsedHeaders;
}
export function getEngine(): AiEngine {
const config = getConfig();
const provider = config.OCO_AI_PROVIDER;
const customHeaders = parseCustomHeaders(config.OCO_API_CUSTOM_HEADERS);
const DEFAULT_CONFIG = {
model: config.OCO_MODEL!,
maxTokensOutput: config.OCO_TOKENS_MAX_OUTPUT!,
maxTokensInput: config.OCO_TOKENS_MAX_INPUT!,
baseURL: config.OCO_API_URL!,
apiKey: config.OCO_API_KEY!
apiKey: config.OCO_API_KEY!,
customHeaders
};
switch (provider) {
@@ -39,6 +70,24 @@ export function getEngine(): AiEngine {
case OCO_AI_PROVIDER_ENUM.FLOWISE:
return new FlowiseEngine(DEFAULT_CONFIG);
case OCO_AI_PROVIDER_ENUM.GROQ:
return new GroqEngine(DEFAULT_CONFIG);
case OCO_AI_PROVIDER_ENUM.MISTRAL:
return new MistralAiEngine(DEFAULT_CONFIG);
case OCO_AI_PROVIDER_ENUM.MLX:
return new MLXEngine(DEFAULT_CONFIG);
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('Done');
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

@@ -0,0 +1,57 @@
/**
* Removes content wrapped in specified tags from a string
* @param content The content string to process
* @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 {
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) {
depth++;
if (depth === 1) {
skipUntil = content.indexOf(closeTag, i + openTag.length);
i = i + openTag.length - 1; // Skip the opening tag
continue;
}
}
// Check for closing tag
else if (
content.substring(i, i + closeTag.length) === closeTag &&
depth > 0
) {
depth--;
if (depth === 0) {
i = i + closeTag.length - 1; // Skip the closing tag
skipUntil = null;
continue;
}
}
// Only add character if not inside a tag
if (skipUntil === null) {
result += content[i];
}
}
// Normalize multiple spaces/tabs into a single space (preserves newlines), then trim.
result = result.replace(/[ \t]+/g, ' ').trim();
return result as unknown as T;
}

View File

@@ -125,7 +125,7 @@ describe('cli flow to push git branch', () => {
await render('git', ['add index.ts'], { cwd: gitDir });
const { queryByText, findByText, userEvent } = await render(
`OCO_AI_PROVIDER='test' node`,
`OCO_AI_PROVIDER='test' OCO_GITPUSH='true' node`,
[resolve('./out/cli.cjs')],
{ cwd: gitDir }
);
@@ -158,7 +158,7 @@ describe('cli flow to push git branch', () => {
await render('git', ['add index.ts'], { cwd: gitDir });
const { findByText, userEvent } = await render(
`OCO_AI_PROVIDER='test' node`,
`OCO_AI_PROVIDER='test' OCO_GITPUSH='true' node`,
[resolve('./out/cli.cjs')],
{ cwd: gitDir }
);
@@ -186,7 +186,7 @@ describe('cli flow to push git branch', () => {
await render('git', ['add index.ts'], { cwd: gitDir });
const { findByText, userEvent } = await render(
`OCO_AI_PROVIDER='test' node`,
`OCO_AI_PROVIDER='test' OCO_GITPUSH='true' node`,
[resolve('./out/cli.cjs')],
{ cwd: gitDir }
);

View File

@@ -9,7 +9,7 @@ it('cli flow to generate commit message for 1 new file (staged)', async () => {
await render('echo' ,[`'console.log("Hello World");' > index.ts`], { cwd: gitDir });
await render('git' ,['add index.ts'], { cwd: gitDir });
const { queryByText, findByText, userEvent } = await render(`OCO_AI_PROVIDER='test' node`, [resolve('./out/cli.cjs')], { cwd: gitDir });
const { queryByText, findByText, userEvent } = await render(`OCO_AI_PROVIDER='test' OCO_GITPUSH='true' node`, [resolve('./out/cli.cjs')], { cwd: gitDir });
expect(await queryByText('No files are staged')).not.toBeInTheConsole();
expect(await queryByText('Do you want to stage all files and generate commit message?')).not.toBeInTheConsole();
@@ -34,7 +34,7 @@ it('cli flow to generate commit message for 1 changed file (not staged)', async
await render('echo' ,[`'console.log("Good night World");' >> index.ts`], { cwd: gitDir });
const { findByText, userEvent } = await render(`OCO_AI_PROVIDER='test' node`, [resolve('./out/cli.cjs')], { cwd: gitDir });
const { findByText, userEvent } = await render(`OCO_AI_PROVIDER='test' OCO_GITPUSH='true' node`, [resolve('./out/cli.cjs')], { cwd: gitDir });
expect(await findByText('No files are staged')).toBeInTheConsole();
expect(await findByText('Do you want to stage all files and generate commit message?')).toBeInTheConsole();

View File

@@ -5,8 +5,8 @@ import { prepareEnvironment, wait } from '../utils';
import path from 'path';
function getAbsolutePath(relativePath: string) {
const scriptDir = path.dirname(__filename);
return path.resolve(scriptDir, relativePath);
// Use process.cwd() which should be the project root during test execution
return path.resolve(process.cwd(), 'test/e2e/prompt-module', relativePath);
}
async function setupCommitlint(dir: string, ver: 9 | 18 | 19) {
let packagePath, packageJsonPath, configPath;
@@ -47,7 +47,7 @@ describe('cli flow to run "oco commitlint force"', () => {
`
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
OCO_PROMPT_MODULE='@commitlint' \
OCO_AI_PROVIDER='test' \
OCO_AI_PROVIDER='test' OCO_GITPUSH='true' \
node ${resolve('./out/cli.cjs')} commitlint force \
`,
[],
@@ -83,7 +83,7 @@ describe('cli flow to run "oco commitlint force"', () => {
`
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
OCO_PROMPT_MODULE='@commitlint' \
OCO_AI_PROVIDER='test' \
OCO_AI_PROVIDER='test' OCO_GITPUSH='true' \
node ${resolve('./out/cli.cjs')} commitlint force \
`,
[],
@@ -119,7 +119,7 @@ describe('cli flow to run "oco commitlint force"', () => {
`
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
OCO_PROMPT_MODULE='@commitlint' \
OCO_AI_PROVIDER='test' \
OCO_AI_PROVIDER='test' OCO_GITPUSH='true' \
node ${resolve('./out/cli.cjs')} commitlint force \
`,
[],
@@ -160,7 +160,7 @@ describe('cli flow to generate commit message using @commitlint prompt-module',
`
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
OCO_PROMPT_MODULE='@commitlint' \
OCO_AI_PROVIDER='test' \
OCO_AI_PROVIDER='test' OCO_GITPUSH='true' \
node ${resolve('./out/cli.cjs')} commitlint force \
`,
[],
@@ -175,7 +175,7 @@ describe('cli flow to generate commit message using @commitlint prompt-module',
`
OCO_TEST_MOCK_TYPE='prompt-module-commitlint-config' \
OCO_PROMPT_MODULE='@commitlint' \
OCO_AI_PROVIDER='test' \
OCO_AI_PROVIDER='test' OCO_GITPUSH='true' \
node ${resolve('./out/cli.cjs')} commitlint get \
`,
[],
@@ -193,7 +193,7 @@ describe('cli flow to generate commit message using @commitlint prompt-module',
`
OCO_TEST_MOCK_TYPE='commit-message' \
OCO_PROMPT_MODULE='@commitlint' \
OCO_AI_PROVIDER='test' \
OCO_AI_PROVIDER='test' OCO_GITPUSH='true' \
node ${resolve('./out/cli.cjs')} \
`,
[],

View File

@@ -1,10 +1,11 @@
import 'cli-testing-library/extend-expect'
import { configure } from 'cli-testing-library'
import { jest } from '@jest/globals';
import 'cli-testing-library/extend-expect';
import { configure } from 'cli-testing-library';
// Make Jest available globally
global.jest = jest;
/**
* Adjusted the wait time for waitFor/findByText to 2000ms, because the default 1000ms makes the test results flaky
*/
configure({ asyncUtilTimeout: 2000 })
configure({ asyncUtilTimeout: 2000 });

View File

@@ -106,7 +106,8 @@ describe('config', () => {
envConfigFile = await generateConfig('.env', {
OCO_TOKENS_MAX_INPUT: '8192',
OCO_ONE_LINE_COMMIT: 'false'
OCO_ONE_LINE_COMMIT: 'false',
OCO_OMIT_SCOPE: 'true'
});
const config = getConfig({
@@ -119,6 +120,31 @@ describe('config', () => {
expect(config.OCO_TOKENS_MAX_OUTPUT).toEqual(500);
expect(config.OCO_GITPUSH).toEqual(true);
expect(config.OCO_ONE_LINE_COMMIT).toEqual(false);
expect(config.OCO_OMIT_SCOPE).toEqual(true);
});
it('should handle custom HTTP headers correctly', async () => {
globalConfigFile = await generateConfig('.opencommit', {
OCO_API_CUSTOM_HEADERS: '{"X-Global-Header": "global-value"}'
});
envConfigFile = await generateConfig('.env', {
OCO_API_CUSTOM_HEADERS: '{"Authorization": "Bearer token123", "X-Custom-Header": "test-value"}'
});
const config = getConfig({
globalPath: globalConfigFile.filePath,
envPath: envConfigFile.filePath
});
expect(config).not.toEqual(null);
expect(config.OCO_API_CUSTOM_HEADERS).toEqual({"Authorization": "Bearer token123", "X-Custom-Header": "test-value"});
// No need to parse JSON again since it's already an object
const parsedHeaders = config.OCO_API_CUSTOM_HEADERS;
expect(parsedHeaders).toHaveProperty('Authorization', 'Bearer token123');
expect(parsedHeaders).toHaveProperty('X-Custom-Header', 'test-value');
expect(parsedHeaders).not.toHaveProperty('X-Global-Header');
});
it('should handle empty local config correctly', async () => {

View File

@@ -0,0 +1,57 @@
import { removeContentTags } from '../../src/utils/removeContentTags';
describe('removeContentTags', () => {
it('should remove content wrapped in specified tags', () => {
const content = 'This is <think>something to hide</think> visible content';
const result = removeContentTags(content, 'think');
expect(result).toBe('This is visible content');
});
it('should handle multiple tag occurrences', () => {
const content = '<think>hidden</think> visible <think>also hidden</think> text';
const result = removeContentTags(content, 'think');
expect(result).toBe('visible text');
});
it('should handle multiline content within tags', () => {
const content = 'Start <think>hidden\nover multiple\nlines</think> End';
const result = removeContentTags(content, 'think');
expect(result).toBe('Start End');
});
it('should return content as is when tag is not found', () => {
const content = 'Content without any tags';
const result = removeContentTags(content, 'think');
expect(result).toBe('Content without any tags');
});
it('should work with different tag names', () => {
const content = 'This is <custom>something to hide</custom> visible content';
const result = removeContentTags(content, 'custom');
expect(result).toBe('This is visible content');
});
it('should handle null content', () => {
const content = null;
const result = removeContentTags(content, 'think');
expect(result).toBe(null);
});
it('should handle undefined content', () => {
const content = undefined;
const result = removeContentTags(content, 'think');
expect(result).toBe(undefined);
});
it('should trim the result', () => {
const content = ' <think>hidden</think> visible ';
const result = removeContentTags(content, 'think');
expect(result).toBe('visible');
});
it('should handle nested tags correctly', () => {
const content = 'Outside <think>Inside <think>Nested</think></think> End';
const result = removeContentTags(content, 'think');
expect(result).toBe('Outside End');
});
});

View File

@@ -3,10 +3,10 @@
"target": "ES2020",
"lib": ["ES6", "ES2020"],
"module": "CommonJS",
"module": "NodeNext",
"resolveJsonModule": true,
"moduleResolution": "Node",
"moduleResolution": "NodeNext",
"allowJs": true,