From 0ed3050bdd749838555c70dac24d04d0e3150555 Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Thu, 12 Mar 2026 18:23:59 +0200
Subject: [PATCH 01/43] update: publish.yml
---
.github/workflows/publish.yml | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 402e848..9047a78 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -23,8 +23,5 @@ jobs:
run: python -m build
- name: Publish package
- env:
- TWINE_USERNAME: __token__
- TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: |
- twine upload dist/*
\ No newline at end of file
+ twine upload dist/* -u __token__ -p ${{ secrets.PYPI_API_TOKEN }}
\ No newline at end of file
From fb19f03ee07cc3a5218367950d14d5398ec51372 Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Thu, 12 Mar 2026 18:25:45 +0200
Subject: [PATCH 02/43] update version
---
pyproject.toml | 2 +-
setup.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index af2c7da..a930b48 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "codefox"
-version = "0.4.0"
+version = "0.4.1"
description = "CodeFox CLI - code auditing and code review tool"
readme = "README.md"
requires-python = ">=3.11"
diff --git a/setup.py b/setup.py
index 3186c9a..667a543 100644
--- a/setup.py
+++ b/setup.py
@@ -7,7 +7,7 @@
setup(
name="codefox",
- version="0.4.0",
+ version="0.4.1",
description="CodeFox CLI - code auditing and code review tool",
long_description=README,
long_description_content_type="text/plain",
From 6f9f2412a90cd394f5ad0b92a39c8b6a0b90dc8c Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Thu, 12 Mar 2026 18:27:18 +0200
Subject: [PATCH 03/43] update: publish.yml
---
.github/workflows/publish.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 9047a78..c974ea5 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -24,4 +24,4 @@ jobs:
- name: Publish package
run: |
- twine upload dist/* -u __token__ -p ${{ secrets.PYPI_API_TOKEN }}
\ No newline at end of file
+ twine upload dist/* -u __token__ -p ${{ secrets.PYPI_API_TOKEN }} --verbose
\ No newline at end of file
From c9d38853098fe2035e1475fbaf46964b22781a4f Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Thu, 12 Mar 2026 18:28:21 +0200
Subject: [PATCH 04/43] update: publish.yml
---
.github/workflows/publish.yml | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index c974ea5..402e848 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -23,5 +23,8 @@ jobs:
run: python -m build
- name: Publish package
+ env:
+ TWINE_USERNAME: __token__
+ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: |
- twine upload dist/* -u __token__ -p ${{ secrets.PYPI_API_TOKEN }} --verbose
\ No newline at end of file
+ twine upload dist/*
\ No newline at end of file
From fb4eb51d5a5148d74003a1cf8ea0685dc43ae5c6 Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Thu, 12 Mar 2026 18:33:03 +0200
Subject: [PATCH 05/43] update version to 0.4.0 in pyproject.toml and setup.py
---
pyproject.toml | 2 +-
setup.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index a930b48..af2c7da 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "codefox"
-version = "0.4.1"
+version = "0.4.0"
description = "CodeFox CLI - code auditing and code review tool"
readme = "README.md"
requires-python = ">=3.11"
diff --git a/setup.py b/setup.py
index 667a543..3186c9a 100644
--- a/setup.py
+++ b/setup.py
@@ -7,7 +7,7 @@
setup(
name="codefox",
- version="0.4.1",
+ version="0.4.0",
description="CodeFox CLI - code auditing and code review tool",
long_description=README,
long_description_content_type="text/plain",
From 5e416e9b1b7ef61a147b880bd7b1c421bafefb33 Mon Sep 17 00:00:00 2001
From: URLBug <79089275+URLbug@users.noreply.github.com>
Date: Sat, 14 Mar 2026 17:20:37 +0200
Subject: [PATCH 06/43] Update README.md
---
README.md | 107 ++++++++++++++++++++++++++++++++++++------------------
1 file changed, 71 insertions(+), 36 deletions(-)
diff --git a/README.md b/README.md
index 1b0d929..3df93d5 100644
--- a/README.md
+++ b/README.md
@@ -4,14 +4,14 @@
CodeFox-CLI
- Intelligent automated code review system
+ Diff-aware AI code review for terminal and CI workflows
-
+
@@ -21,18 +21,35 @@
🐛 Report Issue
+---
## 🦊 Overview
-**CodeFox-CLI** is an intelligent automated code review system that takes over routine security and code quality checks, allowing senior developers to focus on architecture and complex tasks.
+**CodeFox-CLI** is a CLI-first AI code review tool for **git diffs, pull requests, and CI workflows**.
+
+It analyzes code changes, retrieves relevant project context, and produces review feedback directly in the terminal or inside automated review pipelines.
+
+CodeFox supports both:
+- **local reviews with Ollama** for self-hosted workflows
+- **cloud LLM providers** such as Gemini and OpenRouter when remote inference is preferred
-Unlike traditional linters, CodeFox understands the context of the entire project and its business logic, delivering not just review comments but **ready-to-apply fixes** (Auto-Fix). Works with **Gemini**, **Ollama**, and **OpenRouter** - use your preferred AI backend.
+It is designed for developers and teams who want a **CLI-first review workflow** for local checks, pull requests, and CI/CD pipelines.
+
+---
-| vs Linters | vs AI code review (e.g. CodeRabbit) |
-|------------|-------------------------------------|
-| Understands full project context & business logic | Self-hosted / local (Ollama), no vendor lock-in |
-| Suggests fixes, not only rules | Configurable models, security/performance/style rules |
-| RAG over your codebase for relevant hints | CLI-first: `git diff` → review in seconds |
+## Why CodeFox?
+
+- Reviews **git changes**, not just isolated files
+- Uses **relevant codebase context** to improve review quality
+- Works with **local or cloud models**
+- Fits naturally into **terminal-based and CI workflows**
+- Supports configurable review focus such as **security**, **performance**, and **style**
+
+| Compared to linters | Compared to hosted AI reviewers |
+|---|---|
+| Reviews diffs with codebase context, not only static rules | Can run locally with Ollama |
+| Can suggest fixes, not only flag issues | No hard vendor lock-in |
+| Flexible review focus: security, performance, style | CLI-first workflow for local and CI usage |
@@ -40,65 +57,83 @@ Unlike traditional linters, CodeFox understands the context of the entire projec
---
-## 📥 Installation
+## What CodeFox is and is not
-Choose the installation method that fits your workflow.
+CodeFox is a **CLI for automated AI review of git changes**.
-### 🔹 Install dependencies (local setup)
+It is **not** an IDE coding assistant like Cursor or Claude Code.
+It is built for **diff review workflows**, terminal usage, and CI/CD automation.
-```bash
-pip install -r requirements.txt
-```
-### 🔹 Development mode (editable install)
+---
-Provides the local codefox CLI command and enables live code changes.
+## Integrations
-```bash
-python3 -m pip install -e .
-```
+Current:
+- GitHub Actions
-### 🔹 Install from GitHub
+Planned:
+- GitLab
+- Bitbucket
-🐍 Using pip
+---
+
+## Privacy
+
+- With **Ollama**, reviews can run fully locally on your machine
+- With **cloud providers**, code and context may be sent to external APIs depending on your configuration
+- Use `.codefoxignore` to exclude files from analysis
+
+---
+## 📥 Installation
+
+### For users
+
+**uv**
```bash
-python3 -m pip install codefox
-# or python3 -m pip install git+https://github.com/URLbug/CodeFox-CLI.git@main
+uv tool install codefox
```
-⚡ Using uv (recommended for CLI usage)
+**pip**
```bash
-uv tool install codefox
-# or uv tool install git+https://github.com/URLbug/CodeFox-CLI.git@main
+python3 -m pip install codefox
```
---
-✅ Verify installation
+## Verify installation
+
```bash
codefox version
```
-Or
-```bash
-python3 -m codefox version
-```
-## 🚀 Quick Start
+---
-### Initialize (stores your API key)
+## 🚀 Quick Start
+1. Initialize CodeFox
```bash
codefox init
```
-### Run a scan (uses the current git diff)
+This stores your provider token locally and creates the initial config files.
+2. Review your current git changes
```bash
codefox scan
```
-### Show version
+What happens during `scan`:
+
+- collects the current git diff
+
+- loads relevant project context based on your configuration
+
+- sends the review request to the configured model
+
+- returns review comments and optional fix suggestions
+3. Show version
```bash
codefox version
```
From 56dcc2009d1d9fbbb6fae0b7c24773763eb885ab Mon Sep 17 00:00:00 2001
From: URLBug <79089275+URLbug@users.noreply.github.com>
Date: Sat, 14 Mar 2026 18:00:22 +0200
Subject: [PATCH 07/43] Update README.md
---
README.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 3df93d5..d6a5833 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,8 @@
📚 Documentation •
🚀 Quick Start •
- 🐛 Report Issue
+ 🐛 Report Issue •
+ 📝 Demo Pull Request
---
From 6733aac39f2d3649ad32bfff9ee03b3bccff45d7 Mon Sep 17 00:00:00 2001
From: URLBug <79089275+URLbug@users.noreply.github.com>
Date: Sat, 14 Mar 2026 18:01:45 +0200
Subject: [PATCH 08/43] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index d6a5833..4de8ff3 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@
📚 Documentation •
🚀 Quick Start •
🐛 Report Issue •
- 📝 Demo Pull Request
+ 📝 Demo PRs
---
From abf412f6165cd464b1a2c26c48eaac11f06c37a5 Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Sat, 14 Mar 2026 20:53:42 +0200
Subject: [PATCH 09/43] chore: enhance package metadata
---
pyproject.toml | 36 +++++++++++++++++++++++++++++++++++-
setup.py | 31 ++++++++++++++++++++++++++++---
2 files changed, 63 insertions(+), 4 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index af2c7da..0d8395e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -2,8 +2,35 @@
name = "codefox"
version = "0.4.0"
description = "CodeFox CLI - code auditing and code review tool"
-readme = "README.md"
+readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
+license = { text = "MIT" }
+authors = [
+ { name = "CodeFox" },
+]
+keywords = [
+ "ai",
+ "code review",
+ "cli",
+ "static analysis",
+ "ollama",
+ "openai",
+ "gemini",
+]
+classifiers = [
+ "Development Status :: 4 - Beta",
+ "Environment :: Console",
+ "Intended Audience :: Developers",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: OS Independent",
+ "Topic :: Software Development",
+ "Topic :: Software Development :: Quality Assurance",
+ "Topic :: Utilities",
+]
dependencies = [
"bm25s==0.3.0",
"qdrant-client>=1.7.0",
@@ -38,6 +65,13 @@ dev = [
[project.scripts]
codefox = "codefox.main:cli"
+[project.urls]
+Documentation = "https://github.com/codefox-lab/CodeFox-CLI/wiki"
+Homepage = "https://github.com/URLbug/CodeFox-CLI"
+Issues = "https://github.com/URLbug/CodeFox-CLI/issues"
+Security = "https://github.com/URLbug/CodeFox-CLI/blob/main/SECURITY.md"
+Source = "https://github.com/URLbug/CodeFox-CLI"
+
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
diff --git a/setup.py b/setup.py
index 3186c9a..1fdc825 100644
--- a/setup.py
+++ b/setup.py
@@ -3,15 +3,32 @@
from setuptools import find_packages, setup
HERE = pathlib.Path(__file__).parent
-README = (HERE / "README.txt").read_text(encoding="utf-8") if (HERE / "README.txt").exists() else ""
+README = (HERE / "README.md").read_text(encoding="utf-8") if (HERE / "README.md").exists() else ""
setup(
name="codefox",
version="0.4.0",
description="CodeFox CLI - code auditing and code review tool",
long_description=README,
- long_description_content_type="text/plain",
+ long_description_content_type="text/markdown",
author="CodeFox",
+ license="MIT",
+ url="https://github.com/URLbug/CodeFox-CLI",
+ project_urls={
+ "Documentation": "https://github.com/codefox-lab/CodeFox-CLI/wiki",
+ "Source": "https://github.com/URLbug/CodeFox-CLI",
+ "Issues": "https://github.com/URLbug/CodeFox-CLI/issues",
+ "Security": "https://github.com/URLbug/CodeFox-CLI/blob/main/SECURITY.md",
+ },
+ keywords=[
+ "ai",
+ "code review",
+ "cli",
+ "static analysis",
+ "ollama",
+ "openrouter",
+ "gemini",
+ ],
packages=find_packages(),
include_package_data=True,
install_requires=[
@@ -40,10 +57,18 @@
],
},
classifiers=[
+ "Development Status :: 4 - Beta",
+ "Environment :: Console",
+ "Intended Audience :: Developers",
"Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
+ "Topic :: Software Development",
+ "Topic :: Software Development :: Quality Assurance",
+ "Topic :: Utilities",
],
python_requires=">=3.11",
)
-
From 5f0ab9b372ec3b96b0a483c4e1e6fd06cf9b7287 Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Sat, 14 Mar 2026 20:58:26 +0200
Subject: [PATCH 10/43] chore: update minor version
---
pyproject.toml | 2 +-
setup.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 0d8395e..63e3802 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "codefox"
-version = "0.4.0"
+version = "0.4.1"
description = "CodeFox CLI - code auditing and code review tool"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
diff --git a/setup.py b/setup.py
index 1fdc825..7d099e8 100644
--- a/setup.py
+++ b/setup.py
@@ -7,7 +7,7 @@
setup(
name="codefox",
- version="0.4.0",
+ version="0.4.1",
description="CodeFox CLI - code auditing and code review tool",
long_description=README,
long_description_content_type="text/markdown",
From 42f1b13ad19ca2976726d53896efd70cef30c472 Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Mon, 16 Mar 2026 11:57:29 +0200
Subject: [PATCH 11/43] chore: updated action.yml
---
action.yml | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/action.yml b/action.yml
index 84dae80..9c99f42 100644
--- a/action.yml
+++ b/action.yml
@@ -11,12 +11,12 @@ inputs:
required: true
codefox-branch:
- description: 'Git branch of the CodeFox repository to install and run (e.g., main, dev, or a specific release branch).'
+ description: 'Git branch of the CodeFox repository to install and run.'
default: 'main'
required: false
github-token:
- description: 'GitHub token used to authenticate with the GitHub API for posting review comments and interacting with pull requests.'
+ description: 'GitHub token used to authenticate with the GitHub API.'
default: ${{ github.token }}
required: false
@@ -28,6 +28,16 @@ runs:
with:
python-version: '3.12'
+ - name: Restore CodeFox cache
+ uses: actions/cache@v4
+ with:
+ path: .codefox
+ key: ${{ runner.os }}-codefox-${{ github.repository }}-${{ github.base_ref }}-${{ inputs.codefox-branch }}
+ restore-keys: |
+ ${{ runner.os }}-codefox-${{ github.repository }}-${{ github.base_ref }}-
+ ${{ runner.os }}-codefox-${{ github.repository }}-
+ ${{ runner.os }}-codefox-
+
- name: Install CodeFox
shell: bash
run: |
From 9939eda3a291c316e903855cf8589de3e66e333b Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Mon, 16 Mar 2026 14:26:13 +0200
Subject: [PATCH 12/43] update
---
action.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/action.yml b/action.yml
index 9c99f42..8994282 100644
--- a/action.yml
+++ b/action.yml
@@ -31,7 +31,7 @@ runs:
- name: Restore CodeFox cache
uses: actions/cache@v4
with:
- path: .codefox
+ path: ${{ pwd }}/.codefox/
key: ${{ runner.os }}-codefox-${{ github.repository }}-${{ github.base_ref }}-${{ inputs.codefox-branch }}
restore-keys: |
${{ runner.os }}-codefox-${{ github.repository }}-${{ github.base_ref }}-
From 223ddc3b8d14f63e6bb085d947c6c4b43d8a982a Mon Sep 17 00:00:00 2001
From: Rui Carter
Date: Tue, 17 Mar 2026 00:05:07 +0300
Subject: [PATCH 13/43] chore: add new command for index file. Add template for
gitlab ci/cd. Add new params for command init and add new execute errors for
method Helper.get_diff
---
.gitignore | 15 ++++++----
codefox-template.yml | 23 +++++++++++++++
codefox/cli/index.py | 25 ++++++++++++++++
codefox/cli/init.py | 17 +++++++++--
codefox/cli_manager.py | 9 +++++-
codefox/main.py | 30 ++++++++++++++-----
codefox/utils/helper.py | 8 ++++--
codefox/utils/local_rag.py | 59 +++++++++++++++++++-------------------
8 files changed, 138 insertions(+), 48 deletions(-)
create mode 100644 codefox-template.yml
create mode 100644 codefox/cli/index.py
diff --git a/.gitignore b/.gitignore
index dea46fc..0e2db86 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,15 +1,18 @@
.vscode/
.idea/
.venv/
+.codefox/
.*_cache/
-.env
+
+__pycache__/
+build/
+codefox.egg-info/
+
.codefoxenv
.codefoxignore
.codefox.yml
+.env
venv*/
-build/
-__pycache__/
-*.pyc
-.codefox/
-codefox.egg-info/
\ No newline at end of file
+
+*.pyc
\ No newline at end of file
diff --git a/codefox-template.yml b/codefox-template.yml
new file mode 100644
index 0000000..6b993c6
--- /dev/null
+++ b/codefox-template.yml
@@ -0,0 +1,23 @@
+.codefox_scan:
+ image: python:3.12-slim
+ variables:
+ PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
+ cache:
+ key: "codefox-$CI_PROJECT_ID"
+ paths:
+ - .codefox/
+ - .cache/pip/
+ before_script:
+ - apt-get update && apt-get install -y --no-install-recommends git curl ca-certificates && rm -rf /var/lib/apt/lists/*
+ - python -m pip install --upgrade pip
+ - pip install "git+https://github.com/URLbug/CodeFox-CLI.git@${CODEFOX_BRANCH}"
+ - |
+ umask 077
+ {
+ echo "CODEFOX_API_KEY=$CODEFOX_API_KEY"
+ echo "GITLAB_TOKEN=$GITLAB_TOKEN"
+ } > .codefoxenv
+ script:
+ - git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME":"$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" || true
+ - git fetch origin "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME":"$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" || true
+ - codefox scan --target-branch "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" --source-branch "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" --ci
diff --git a/codefox/cli/index.py b/codefox/cli/index.py
new file mode 100644
index 0000000..8e35fc6
--- /dev/null
+++ b/codefox/cli/index.py
@@ -0,0 +1,25 @@
+import os
+from rich import print
+from rich.markup import escape
+
+from codefox.api.base_api import BaseAPI
+from codefox.cli.base_cli import BaseCLI
+
+
+class Index(BaseCLI):
+ def __init__(self, model: type[BaseAPI]):
+ self.model = model()
+
+ def execute(self):
+ is_upload, error = self.model.upload_files(os.getcwd())
+ if not is_upload:
+ print(
+ "[red]Failed to index files: "
+ + escape(str(error))
+ + "[/red]"
+ )
+ return
+
+ print(
+ "[green]Successful to index files[/green]"
+ )
diff --git a/codefox/cli/init.py b/codefox/cli/init.py
index 0d9b6b5..ac8971a 100644
--- a/codefox/cli/init.py
+++ b/codefox/cli/init.py
@@ -1,3 +1,4 @@
+from typing import Any
from pathlib import Path
import yaml
@@ -11,9 +12,16 @@
class Init(BaseCLI):
- def __init__(self, model_enum: ModelEnum | None = None):
- self.model_enum = model_enum or self._ask_model()
+ def __init__(self, args: dict[str, Any] | None = None):
+ if not args.get("provider"):
+ self.model_enum = self._ask_model()
+ else:
+ self.model_enum = ModelEnum.by_name(args["provider"])
+
self.api_class: type[BaseAPI] = self.model_enum.api_class
+
+ self.args = args
+
self.config_path = Path(".codefoxenv")
self.ignore_path = Path(".codefoxignore")
self.yaml_config_path = Path(".codefox.yml")
@@ -28,7 +36,10 @@ def _ask_model(self) -> ModelEnum:
return ModelEnum.by_name(choice)
def execute(self) -> None:
- api_key = self._ask_api_key() or ""
+ if not self.args.get("token"):
+ api_key = self._ask_api_key() or ""
+ else:
+ api_key = self.args["token"]
if not self._write_config(api_key):
return
diff --git a/codefox/cli_manager.py b/codefox/cli_manager.py
index 935cebb..b53fa91 100644
--- a/codefox/cli_manager.py
+++ b/codefox/cli_manager.py
@@ -11,6 +11,7 @@
from codefox.cli.init import Init
from codefox.cli.list import List
from codefox.cli.scan import Scan
+from codefox.cli.index import Index
from codefox.utils.helper import Helper
@@ -34,6 +35,12 @@ def run(self) -> None:
version = importlib.metadata.version("codefox")
print(f"[green]CodeFox CLI version {version}[/green]")
return
+
+ if self.command == "index":
+ api_class = self._get_api_class()
+ index = Index(api_class)
+ index.execute()
+ return
if self.command == "list":
api_class = self._get_api_class()
@@ -54,7 +61,7 @@ def run(self) -> None:
return
if self.command == "init":
- init = Init()
+ init = Init(self.args or {})
init.execute()
return
diff --git a/codefox/main.py b/codefox/main.py
index 2344945..e1f0db6 100755
--- a/codefox/main.py
+++ b/codefox/main.py
@@ -8,9 +8,9 @@
@app.command("scan")
def scan(
- ci: bool = typer.Option(False, "--ci", help="CI mode"),
- source_branch: str = typer.Option(None, help="Source branch"),
- target_branch: str = typer.Option(None, help="Target branch"),
+ ci: bool = typer.Option(False, "--ci", help="CI mode."),
+ source_branch: str | None = typer.Option(None, help="Source branch."),
+ target_branch: str | None = typer.Option(None, help="Target branch."),
):
"""Run AI code review."""
manager = CLIManager(
@@ -23,15 +23,31 @@ def scan(
)
manager.run()
+@app.command("index")
+def index():
+ """Index files"""
+ CLIManager(
+ command="index", args={}
+ ).run()
@app.command("init")
-def init():
+def init(
+ provider: str | None = typer.Option(None, help="Provider."),
+ token: str | None = typer.Option(None, help="Token provider.")
+):
"""Initialize CodeFox."""
- CLIManager(command="init", args={}).run()
+ manager = CLIManager(
+ command="init",
+ args={
+ "provider": provider,
+ "token": token,
+ }
+ )
+ manager.run()
@app.command("list")
-def list_models(type_model: str = typer.Argument("models", help="Model type")):
+def list_models(type_model: str = typer.Argument("models", help="Model type.")):
"""List available models."""
manager = CLIManager(
command="list",
@@ -43,7 +59,7 @@ def list_models(type_model: str = typer.Argument("models", help="Model type")):
@app.command("clean")
-def clean(type_cache: str = typer.Argument("all", help="Cache type")):
+def clean(type_cache: str = typer.Argument("all", help="Cache type.")):
"""Clean cache"""
manager = CLIManager(
command="clean",
diff --git a/codefox/utils/helper.py b/codefox/utils/helper.py
index 6e4422c..2f4e96b 100644
--- a/codefox/utils/helper.py
+++ b/codefox/utils/helper.py
@@ -65,7 +65,7 @@ def get_diff(
source_branch: str | None = None, target_branch: str | None = None
) -> str | None:
try:
- repo = git.Repo(".")
+ repo = git.Repo(".", search_parent_directories=True)
if source_branch and target_branch:
diff_text = repo.git.diff(
@@ -75,7 +75,11 @@ def get_diff(
diff_text = repo.git.diff()
return cast(str | None, diff_text)
- except git.exc.InvalidGitRepositoryError:
+ except (
+ git.exc.InvalidGitRepositoryError,
+ git.exc.NoSuchPathError,
+ git.exc.GitCommandError,
+ ):
return None
# ------------------------------------------------------------------
diff --git a/codefox/utils/local_rag.py b/codefox/utils/local_rag.py
index 27942ce..dcdde31 100644
--- a/codefox/utils/local_rag.py
+++ b/codefox/utils/local_rag.py
@@ -191,40 +191,41 @@ def build(self) -> None:
if self.client.collection_exists(self.collection_name):
self.client.delete_collection(self.collection_name)
- with self.console.status(
+ self.console.print(
"[magenta]Building Qdrant vector index...[/magenta]"
+ )
+
+ dim: int | None = None
+ for i in track(
+ range(0, len(texts), batch_size),
+ total=(len(texts) + batch_size - 1) // batch_size,
+ description="[blue]Generating embeddings...[/blue]",
):
- dim: int | None = None
- for i in track(
- range(0, len(texts), batch_size),
- total=(len(texts) + batch_size - 1) // batch_size,
- description="[blue]Generating embeddings...[/blue]",
- ):
- batch = texts[i : i + batch_size]
- emb = np.array(list(self.model.embed(batch)), dtype="float32")
-
- if dim is None:
- dim = emb.shape[1]
- self.client.create_collection(
- collection_name=self.collection_name,
- vectors_config=VectorParams(
- size=dim, distance=Distance.COSINE
- ),
- )
-
- points = [
- PointStruct(
- id=j,
- vector=vec.tolist(),
- payload={"path": self.files[j]["path"]},
- )
- for j, vec in enumerate(emb, start=i)
- ]
- self.client.upsert(
+ batch = texts[i : i + batch_size]
+ emb = np.array(list(self.model.embed(batch)), dtype="float32")
+
+ if dim is None:
+ dim = emb.shape[1]
+ self.client.create_collection(
collection_name=self.collection_name,
- points=points,
+ vectors_config=VectorParams(
+ size=dim, distance=Distance.COSINE
+ ),
)
+ points = [
+ PointStruct(
+ id=j,
+ vector=vec.tolist(),
+ payload={"path": self.files[j]["path"]},
+ )
+ for j, vec in enumerate(emb, start=i)
+ ]
+ self.client.upsert(
+ collection_name=self.collection_name,
+ points=points,
+ )
+
self.console.print("[green]✓[/green] Qdrant semantic index built.")
self.console.print(
"[bold green]RAG build complete and ready for "
From 1ab6ae20686f5ac21355fd288856eb9b616ed3ff Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 12:31:31 +0200
Subject: [PATCH 14/43] feat: add stage to codefox-template.yml
---
codefox-template.yml | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/codefox-template.yml b/codefox-template.yml
index 6b993c6..25c2800 100644
--- a/codefox-template.yml
+++ b/codefox-template.yml
@@ -1,4 +1,8 @@
-.codefox_scan:
+stage:
+ - review
+
+codefox_review:
+ stage: review
image: python:3.12-slim
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
From f772ab4b265be6586c7e188ea813406f27fdf752 Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 12:35:26 +0200
Subject: [PATCH 15/43] chore: add rulers and fix stages
---
codefox-template.yml | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/codefox-template.yml b/codefox-template.yml
index 25c2800..6c7be4e 100644
--- a/codefox-template.yml
+++ b/codefox-template.yml
@@ -1,16 +1,19 @@
-stage:
+stages:
- review
codefox_review:
stage: review
image: python:3.12-slim
+
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
+
cache:
key: "codefox-$CI_PROJECT_ID"
paths:
- .codefox/
- .cache/pip/
+
before_script:
- apt-get update && apt-get install -y --no-install-recommends git curl ca-certificates && rm -rf /var/lib/apt/lists/*
- python -m pip install --upgrade pip
@@ -21,7 +24,11 @@ codefox_review:
echo "CODEFOX_API_KEY=$CODEFOX_API_KEY"
echo "GITLAB_TOKEN=$GITLAB_TOKEN"
} > .codefoxenv
+
script:
- git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME":"$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" || true
- git fetch origin "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME":"$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" || true
- codefox scan --target-branch "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" --source-branch "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" --ci
+
+ rules:
+ - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
\ No newline at end of file
From 5df777cfd66bfd2b33551b62c94fa4c89f4d5b6f Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 12:50:25 +0200
Subject: [PATCH 16/43] chore: replace gitlab_token to ci_job_token
---
codefox-template.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/codefox-template.yml b/codefox-template.yml
index 6c7be4e..6147ff9 100644
--- a/codefox-template.yml
+++ b/codefox-template.yml
@@ -22,7 +22,7 @@ codefox_review:
umask 077
{
echo "CODEFOX_API_KEY=$CODEFOX_API_KEY"
- echo "GITLAB_TOKEN=$GITLAB_TOKEN"
+ echo "GITLAB_TOKEN=$CI_JOB_TOKEN"
} > .codefoxenv
script:
From ad3838489229fce49ab604d5ed9c5adc0366c90d Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 12:53:13 +0200
Subject: [PATCH 17/43] fix: CODEFOX_BRANCH
---
codefox-template.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/codefox-template.yml b/codefox-template.yml
index 6147ff9..3e14bc0 100644
--- a/codefox-template.yml
+++ b/codefox-template.yml
@@ -17,7 +17,7 @@ codefox_review:
before_script:
- apt-get update && apt-get install -y --no-install-recommends git curl ca-certificates && rm -rf /var/lib/apt/lists/*
- python -m pip install --upgrade pip
- - pip install "git+https://github.com/URLbug/CodeFox-CLI.git@${CODEFOX_BRANCH}"
+ - pip install "git+https://github.com/URLbug/CodeFox-CLI.git@${$CODEFOX_BRANCH}"
- |
umask 077
{
From 4c0b6526459f78b667e5d3182d36e54ae5ecb30f Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 12:56:10 +0200
Subject: [PATCH 18/43] feat: add default variable
---
codefox-template.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/codefox-template.yml b/codefox-template.yml
index 3e14bc0..7c279e8 100644
--- a/codefox-template.yml
+++ b/codefox-template.yml
@@ -17,7 +17,7 @@ codefox_review:
before_script:
- apt-get update && apt-get install -y --no-install-recommends git curl ca-certificates && rm -rf /var/lib/apt/lists/*
- python -m pip install --upgrade pip
- - pip install "git+https://github.com/URLbug/CodeFox-CLI.git@${$CODEFOX_BRANCH}"
+ - pip install "git+https://github.com/URLbug/CodeFox-CLI.git@${CODEFOX_BRANCH:-main}"
- |
umask 077
{
From 4b8d5481a56aaf4ee20fe5d83275b7909506942a Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 13:16:23 +0200
Subject: [PATCH 19/43] feat: make gitlab ci/cd integration
---
codefox-template.yml | 6 +++++-
codefox/bots/gitlab_bot.py | 39 ++++++++++++++++++++++++++++++++++++++
2 files changed, 44 insertions(+), 1 deletion(-)
create mode 100644 codefox/bots/gitlab_bot.py
diff --git a/codefox-template.yml b/codefox-template.yml
index 7c279e8..c9b9b64 100644
--- a/codefox-template.yml
+++ b/codefox-template.yml
@@ -22,7 +22,11 @@ codefox_review:
umask 077
{
echo "CODEFOX_API_KEY=$CODEFOX_API_KEY"
- echo "GITLAB_TOKEN=$CI_JOB_TOKEN"
+ echo "GITLAB_TOKEN=${GITLAB_TOKEN:-$CI_JOB_TOKEN}"
+ echo "GITLAB_BOT=${GITLAB_TOKEN:-$CI_JOB_TOKEN}"
+ echo "GITLAB_URL=${CI_SERVER_URL}"
+ echo "GITLAB_REPOSITORY=${CI_PROJECT_PATH}"
+ echo "PR_NUMBER=${CI_MERGE_REQUEST_IID}"
} > .codefoxenv
script:
diff --git a/codefox/bots/gitlab_bot.py b/codefox/bots/gitlab_bot.py
new file mode 100644
index 0000000..9dfc380
--- /dev/null
+++ b/codefox/bots/gitlab_bot.py
@@ -0,0 +1,39 @@
+import os
+
+from gitlab import Gitlab
+
+
+class GitLabBot:
+ def __init__(self):
+ self.gitlab_token = os.getenv("GITLAB_BOT")
+ self.gitlab_url = os.getenv("GITLAB_URL", "https://gitlab.com/")
+ self.repository = os.getenv("GITLAB_REPOSITORY")
+ self.pr_number = os.getenv("PR_NUMBER")
+
+ if not self.gitlab_token:
+ raise ValueError(
+ "GITLAB_BOT environment variable is not set. "
+ "This token is required to authenticate with the Gitlab API."
+ )
+
+ if not self.repository:
+ raise ValueError(
+ "GITLAB_REPOSITORY environment variable is not set. "
+ "Expected format: 'owner/repository'."
+ )
+
+ self.gitlab = Gitlab(
+ url=self.gitlab_url,
+ private_token=self.gitlab_token
+ )
+
+ def send(self, message: str) -> None:
+ if not self.pr_number or not self.pr_number.isdigit():
+ raise RuntimeError(f"Invalid PR_NUMBER: {self.pr_number}")
+
+ self.gitlab.auth()
+
+ repo = self.gitlab.projects.get(self.repository)
+ mr = repo.mergerequests.get(int(self.pr_number))
+
+ mr.notes.create({"body": message})
From 8b363cf6a3277bd7a6bb0e2c80825b9735c92d6c Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 13:19:47 +0200
Subject: [PATCH 20/43] feat: add gitlab bot to command scan
---
codefox/cli/scan.py | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/codefox/cli/scan.py b/codefox/cli/scan.py
index a34d791..db51112 100644
--- a/codefox/cli/scan.py
+++ b/codefox/cli/scan.py
@@ -9,6 +9,7 @@
from codefox.api.base_api import BaseAPI
from codefox.bots.github_bot import GitHubBot
+from codefox.bots.gitlab_bot import GitLabBot
from codefox.cli.base_cli import BaseCLI
from codefox.cli.list import List
from codefox.utils.helper import Helper
@@ -20,8 +21,11 @@ def __init__(self, model: type[BaseAPI], args: dict[str, Any]):
self.args = args
self.github_bot = None
- if self.args.get("ci", False):
+ self.gitlab_bot = None
+ if self.args.get("ci", False) and os.getenv("GITHUB_BOT"):
self.github_bot = GitHubBot()
+ elif self.args("ci", False) and os.getenv("GITLAB_BOT"):
+ self.gitlab_bot = GitLabBot()
def execute(self) -> None:
source_branch, target_branch = self._get_branchs()
@@ -79,6 +83,10 @@ def _ci_response_answer(self, diff_text: str) -> None:
response = self.model.execute(diff_text)
if self.github_bot is not None:
self.github_bot.send(response.text)
+
+ if self.gitlab_bot is not None:
+ self.gitlab_bot.send(response.text)
+
self.model.remove_files()
def _classic_response_answer(self, diff_text: str) -> None:
From e74b0e154191c97303045de929358c3c330905f6 Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 13:22:31 +0200
Subject: [PATCH 21/43] fix: add python-gitlab sdk to project
---
pyproject.toml | 1 +
requirements.in | 3 ++-
requirements.txt | 3 ++-
setup.py | 1 +
4 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 63e3802..ab9b12f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -50,6 +50,7 @@ dependencies = [
"psutil==7.2.2",
"PyGithub==2.8.1",
"pygments==2.19.2",
+ "python-gitlab==8.1.0",
]
[project.optional-dependencies]
diff --git a/requirements.in b/requirements.in
index df054fd..00355ca 100644
--- a/requirements.in
+++ b/requirements.in
@@ -15,4 +15,5 @@ typer==0.23.1
tree-sitter-language-pack==0.13.0
psutil==7.2.2
PyGithub==2.8.1
-pygments==2.19.2
\ No newline at end of file
+pygments==2.19.2
+python-gitlab==8.1.0
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index df054fd..00355ca 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -15,4 +15,5 @@ typer==0.23.1
tree-sitter-language-pack==0.13.0
psutil==7.2.2
PyGithub==2.8.1
-pygments==2.19.2
\ No newline at end of file
+pygments==2.19.2
+python-gitlab==8.1.0
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 7d099e8..3e18fcb 100644
--- a/setup.py
+++ b/setup.py
@@ -50,6 +50,7 @@
"psutil==7.2.2",
"PyGithub==2.8.1",
"pygments==2.19.2",
+ "python-gitlab==8.1.0",
],
entry_points={
"console_scripts": [
From 62418ce8570fef897c94211491d436ecb7631ec7 Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 13:39:53 +0200
Subject: [PATCH 22/43] debug
---
codefox-template.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/codefox-template.yml b/codefox-template.yml
index c9b9b64..44c18d7 100644
--- a/codefox-template.yml
+++ b/codefox-template.yml
@@ -15,6 +15,8 @@ codefox_review:
- .cache/pip/
before_script:
+ - echo "CODEFOX_BRANCH=[$CODEFOX_BRANCH]"
+ - env | sort | grep '^CODEFOX' || true
- apt-get update && apt-get install -y --no-install-recommends git curl ca-certificates && rm -rf /var/lib/apt/lists/*
- python -m pip install --upgrade pip
- pip install "git+https://github.com/URLbug/CodeFox-CLI.git@${CODEFOX_BRANCH:-main}"
From 18cc9de9b2ce4340742be0b0c3b7abe4aa0a3dcc Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 13:46:09 +0200
Subject: [PATCH 23/43] feat
---
codefox-template.yml | 13 +++++--------
1 file changed, 5 insertions(+), 8 deletions(-)
diff --git a/codefox-template.yml b/codefox-template.yml
index 44c18d7..42b5839 100644
--- a/codefox-template.yml
+++ b/codefox-template.yml
@@ -1,25 +1,22 @@
-stages:
- - review
-
codefox_review:
stage: review
image: python:3.12-slim
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
+ CODEFOX_BRANCH: "dev-docs"
cache:
- key: "codefox-$CI_PROJECT_ID"
+ key: codefox-$CI_PROJECT_ID
paths:
- - .codefox/
- - .cache/pip/
+ - ".codefox/"
+ - ".cache/pip/"
before_script:
- echo "CODEFOX_BRANCH=[$CODEFOX_BRANCH]"
- - env | sort | grep '^CODEFOX' || true
- apt-get update && apt-get install -y --no-install-recommends git curl ca-certificates && rm -rf /var/lib/apt/lists/*
- python -m pip install --upgrade pip
- - pip install "git+https://github.com/URLbug/CodeFox-CLI.git@${CODEFOX_BRANCH:-main}"
+ - pip install "git+https://github.com/URLbug/CodeFox-CLI.git@${CODEFOX_BRANCH}"
- |
umask 077
{
From 7f149a87c674302243e446781416f2f795c81c34 Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 14:03:18 +0200
Subject: [PATCH 24/43] fix: add method get
---
codefox/cli/scan.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/codefox/cli/scan.py b/codefox/cli/scan.py
index db51112..5c78d84 100644
--- a/codefox/cli/scan.py
+++ b/codefox/cli/scan.py
@@ -24,7 +24,7 @@ def __init__(self, model: type[BaseAPI], args: dict[str, Any]):
self.gitlab_bot = None
if self.args.get("ci", False) and os.getenv("GITHUB_BOT"):
self.github_bot = GitHubBot()
- elif self.args("ci", False) and os.getenv("GITLAB_BOT"):
+ elif self.args.get("ci", False) and os.getenv("GITLAB_BOT"):
self.gitlab_bot = GitLabBot()
def execute(self) -> None:
From 10b33c327fd0d0d0748b227ab10da4778f73d3ee Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 14:19:57 +0200
Subject: [PATCH 25/43] feat
---
codefox-template.yml | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/codefox-template.yml b/codefox-template.yml
index 42b5839..f5909f1 100644
--- a/codefox-template.yml
+++ b/codefox-template.yml
@@ -26,7 +26,8 @@ codefox_review:
echo "GITLAB_URL=${CI_SERVER_URL}"
echo "GITLAB_REPOSITORY=${CI_PROJECT_PATH}"
echo "PR_NUMBER=${CI_MERGE_REQUEST_IID}"
- } > .codefoxenv
+ } > "$CI_PROJECT_DIR/.codefoxenv"
+ - ls -la "$CI_PROJECT_DIR"
script:
- git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME":"$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" || true
From b8555c567a059c70ccf6ee9e675b8a9f9c62e88c Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 14:22:37 +0200
Subject: [PATCH 26/43] debug
---
codefox-template.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/codefox-template.yml b/codefox-template.yml
index f5909f1..0c8fea2 100644
--- a/codefox-template.yml
+++ b/codefox-template.yml
@@ -27,7 +27,7 @@ codefox_review:
echo "GITLAB_REPOSITORY=${CI_PROJECT_PATH}"
echo "PR_NUMBER=${CI_MERGE_REQUEST_IID}"
} > "$CI_PROJECT_DIR/.codefoxenv"
- - ls -la "$CI_PROJECT_DIR"
+ - cat "$CODEFOX_API_KEY"
script:
- git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME":"$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" || true
From ae32911a356b071745a84725865886f5569bf75c Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 14:27:47 +0200
Subject: [PATCH 27/43] debug
---
codefox-template.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/codefox-template.yml b/codefox-template.yml
index 0c8fea2..ea6f67b 100644
--- a/codefox-template.yml
+++ b/codefox-template.yml
@@ -27,7 +27,7 @@ codefox_review:
echo "GITLAB_REPOSITORY=${CI_PROJECT_PATH}"
echo "PR_NUMBER=${CI_MERGE_REQUEST_IID}"
} > "$CI_PROJECT_DIR/.codefoxenv"
- - cat "$CODEFOX_API_KEY"
+ - echo "$CODEFOX_API_KEY"
script:
- git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME":"$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" || true
From b45b897a618df00939853f4034e13037d0a8b3b5 Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 14:51:31 +0200
Subject: [PATCH 28/43] debug
---
codefox-template.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/codefox-template.yml b/codefox-template.yml
index ea6f67b..4633d8f 100644
--- a/codefox-template.yml
+++ b/codefox-template.yml
@@ -27,7 +27,7 @@ codefox_review:
echo "GITLAB_REPOSITORY=${CI_PROJECT_PATH}"
echo "PR_NUMBER=${CI_MERGE_REQUEST_IID}"
} > "$CI_PROJECT_DIR/.codefoxenv"
- - echo "$CODEFOX_API_KEY"
+ - echo $CODEFOX_API_KEY
script:
- git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME":"$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" || true
From d04dad5bd0610becc474f107d9f558ddd5987ca8 Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 14:58:50 +0200
Subject: [PATCH 29/43] debug
---
codefox-template.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/codefox-template.yml b/codefox-template.yml
index 4633d8f..ecae5ff 100644
--- a/codefox-template.yml
+++ b/codefox-template.yml
@@ -27,7 +27,7 @@ codefox_review:
echo "GITLAB_REPOSITORY=${CI_PROJECT_PATH}"
echo "PR_NUMBER=${CI_MERGE_REQUEST_IID}"
} > "$CI_PROJECT_DIR/.codefoxenv"
- - echo $CODEFOX_API_KEY
+ - test -f .codefoxenv && echo ".codefoxenv exists"
script:
- git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME":"$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" || true
From 9c61328c37a0bee000f35e4bcad634e4d54d283b Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 16:46:48 +0200
Subject: [PATCH 30/43] feat: disable debug mod
---
codefox-template.yml | 2 --
codefox/bots/gitlab_bot.py | 2 --
2 files changed, 4 deletions(-)
diff --git a/codefox-template.yml b/codefox-template.yml
index ecae5ff..1877adf 100644
--- a/codefox-template.yml
+++ b/codefox-template.yml
@@ -13,7 +13,6 @@ codefox_review:
- ".cache/pip/"
before_script:
- - echo "CODEFOX_BRANCH=[$CODEFOX_BRANCH]"
- apt-get update && apt-get install -y --no-install-recommends git curl ca-certificates && rm -rf /var/lib/apt/lists/*
- python -m pip install --upgrade pip
- pip install "git+https://github.com/URLbug/CodeFox-CLI.git@${CODEFOX_BRANCH}"
@@ -27,7 +26,6 @@ codefox_review:
echo "GITLAB_REPOSITORY=${CI_PROJECT_PATH}"
echo "PR_NUMBER=${CI_MERGE_REQUEST_IID}"
} > "$CI_PROJECT_DIR/.codefoxenv"
- - test -f .codefoxenv && echo ".codefoxenv exists"
script:
- git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME":"$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" || true
diff --git a/codefox/bots/gitlab_bot.py b/codefox/bots/gitlab_bot.py
index 9dfc380..f05730d 100644
--- a/codefox/bots/gitlab_bot.py
+++ b/codefox/bots/gitlab_bot.py
@@ -31,8 +31,6 @@ def send(self, message: str) -> None:
if not self.pr_number or not self.pr_number.isdigit():
raise RuntimeError(f"Invalid PR_NUMBER: {self.pr_number}")
- self.gitlab.auth()
-
repo = self.gitlab.projects.get(self.repository)
mr = repo.mergerequests.get(int(self.pr_number))
From d7f930c0638e42679f8cfe32bee8cb397227cbc3 Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 16:50:54 +0200
Subject: [PATCH 31/43] redfactor
---
codefox/bots/gitlab_bot.py | 47 +++++++++++++++++++++++++++-----------
1 file changed, 34 insertions(+), 13 deletions(-)
diff --git a/codefox/bots/gitlab_bot.py b/codefox/bots/gitlab_bot.py
index f05730d..38e772e 100644
--- a/codefox/bots/gitlab_bot.py
+++ b/codefox/bots/gitlab_bot.py
@@ -1,37 +1,58 @@
import os
+from urllib.parse import quote_plus
from gitlab import Gitlab
+from gitlab.exceptions import GitlabAuthenticationError, GitlabGetError, GitlabCreateError
class GitLabBot:
- def __init__(self):
+ def __init__(self) -> None:
self.gitlab_token = os.getenv("GITLAB_BOT")
- self.gitlab_url = os.getenv("GITLAB_URL", "https://gitlab.com/")
+ self.gitlab_url = os.getenv("GITLAB_URL", "https://gitlab.com")
self.repository = os.getenv("GITLAB_REPOSITORY")
- self.pr_number = os.getenv("PR_NUMBER")
+ self.mr_iid = os.getenv("PR_NUMBER")
if not self.gitlab_token:
raise ValueError(
"GITLAB_BOT environment variable is not set. "
- "This token is required to authenticate with the Gitlab API."
+ "This token is required to authenticate with the GitLab API."
)
if not self.repository:
raise ValueError(
"GITLAB_REPOSITORY environment variable is not set. "
- "Expected format: 'owner/repository'."
+ "Expected format: 'group/project' or 'group/subgroup/project'."
+ )
+
+ if not self.mr_iid or not self.mr_iid.isdigit():
+ raise ValueError(
+ f"Invalid PR_NUMBER value: {self.mr_iid!r}. "
+ "Expected a numeric merge request IID."
)
self.gitlab = Gitlab(
url=self.gitlab_url,
- private_token=self.gitlab_token
+ private_token=self.gitlab_token,
)
- def send(self, message: str) -> None:
- if not self.pr_number or not self.pr_number.isdigit():
- raise RuntimeError(f"Invalid PR_NUMBER: {self.pr_number}")
-
- repo = self.gitlab.projects.get(self.repository)
- mr = repo.mergerequests.get(int(self.pr_number))
+ try:
+ self.gitlab.auth()
+ except GitlabAuthenticationError as exc:
+ raise RuntimeError("Failed to authenticate to GitLab API.") from exc
- mr.notes.create({"body": message})
+ def send(self, message: str) -> None:
+ if not message or not message.strip():
+ raise ValueError("Message must not be empty.")
+
+ project_path = quote_plus(self.repository)
+
+ try:
+ project = self.gitlab.projects.get(project_path)
+ mr = project.mergerequests.get(int(self.mr_iid))
+ mr.notes.create({"body": message})
+ except GitlabGetError as exc:
+ raise RuntimeError(
+ f"Failed to find project '{self.repository}' or merge request IID {self.mr_iid}."
+ ) from exc
+ except GitlabCreateError as exc:
+ raise RuntimeError("Failed to create merge request note.") from exc
From 9200ecec94182794a3dfd2d638d0ee0c64438538 Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 16:57:56 +0200
Subject: [PATCH 32/43] refactor: use job token
---
codefox/bots/gitlab_bot.py | 7 +------
1 file changed, 1 insertion(+), 6 deletions(-)
diff --git a/codefox/bots/gitlab_bot.py b/codefox/bots/gitlab_bot.py
index 38e772e..aeec274 100644
--- a/codefox/bots/gitlab_bot.py
+++ b/codefox/bots/gitlab_bot.py
@@ -32,14 +32,9 @@ def __init__(self) -> None:
self.gitlab = Gitlab(
url=self.gitlab_url,
- private_token=self.gitlab_token,
+ job_token=self.gitlab_token,
)
- try:
- self.gitlab.auth()
- except GitlabAuthenticationError as exc:
- raise RuntimeError("Failed to authenticate to GitLab API.") from exc
-
def send(self, message: str) -> None:
if not message or not message.strip():
raise ValueError("Message must not be empty.")
From 12fd1018edc6aeda580635988236f745e35d8807 Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 17:02:48 +0200
Subject: [PATCH 33/43] feat
---
codefox-template.yml | 2 +-
codefox/bots/gitlab_bot.py | 11 ++++-------
2 files changed, 5 insertions(+), 8 deletions(-)
diff --git a/codefox-template.yml b/codefox-template.yml
index 1877adf..4645853 100644
--- a/codefox-template.yml
+++ b/codefox-template.yml
@@ -23,7 +23,7 @@ codefox_review:
echo "GITLAB_TOKEN=${GITLAB_TOKEN:-$CI_JOB_TOKEN}"
echo "GITLAB_BOT=${GITLAB_TOKEN:-$CI_JOB_TOKEN}"
echo "GITLAB_URL=${CI_SERVER_URL}"
- echo "GITLAB_REPOSITORY=${CI_PROJECT_PATH}"
+ echo "GITLAB_REPOSITORY=${CI_PROJECT_ID}"
echo "PR_NUMBER=${CI_MERGE_REQUEST_IID}"
} > "$CI_PROJECT_DIR/.codefoxenv"
diff --git a/codefox/bots/gitlab_bot.py b/codefox/bots/gitlab_bot.py
index aeec274..91f4958 100644
--- a/codefox/bots/gitlab_bot.py
+++ b/codefox/bots/gitlab_bot.py
@@ -2,7 +2,7 @@
from urllib.parse import quote_plus
from gitlab import Gitlab
-from gitlab.exceptions import GitlabAuthenticationError, GitlabGetError, GitlabCreateError
+from gitlab.exceptions import GitlabGetError, GitlabCreateError
class GitLabBot:
@@ -18,11 +18,8 @@ def __init__(self) -> None:
"This token is required to authenticate with the GitLab API."
)
- if not self.repository:
- raise ValueError(
- "GITLAB_REPOSITORY environment variable is not set. "
- "Expected format: 'group/project' or 'group/subgroup/project'."
- )
+ if not self.repository or not self.repository.isdigit():
+ raise ValueError(f"Invalid GITLAB_REPOSITORY: {self.repository!r}")
if not self.mr_iid or not self.mr_iid.isdigit():
raise ValueError(
@@ -42,7 +39,7 @@ def send(self, message: str) -> None:
project_path = quote_plus(self.repository)
try:
- project = self.gitlab.projects.get(project_path)
+ project = self.gitlab.projects.get(int(project_path))
mr = project.mergerequests.get(int(self.mr_iid))
mr.notes.create({"body": message})
except GitlabGetError as exc:
From dbe9b28d98d12498eae07f2b2ccad9f8c8cd3138 Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 17:11:56 +0200
Subject: [PATCH 34/43] feat
---
codefox/bots/gitlab_bot.py | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/codefox/bots/gitlab_bot.py b/codefox/bots/gitlab_bot.py
index 91f4958..c34c632 100644
--- a/codefox/bots/gitlab_bot.py
+++ b/codefox/bots/gitlab_bot.py
@@ -36,10 +36,8 @@ def send(self, message: str) -> None:
if not message or not message.strip():
raise ValueError("Message must not be empty.")
- project_path = quote_plus(self.repository)
-
try:
- project = self.gitlab.projects.get(int(project_path))
+ project = self.gitlab.projects.get(int(self.repository))
mr = project.mergerequests.get(int(self.mr_iid))
mr.notes.create({"body": message})
except GitlabGetError as exc:
From ace19ceca52a349468b3eca7b6689862277e733e Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 17:13:54 +0200
Subject: [PATCH 35/43] debug
---
codefox/bots/gitlab_bot.py | 21 +++++++++++----------
1 file changed, 11 insertions(+), 10 deletions(-)
diff --git a/codefox/bots/gitlab_bot.py b/codefox/bots/gitlab_bot.py
index c34c632..5a7b614 100644
--- a/codefox/bots/gitlab_bot.py
+++ b/codefox/bots/gitlab_bot.py
@@ -36,13 +36,14 @@ def send(self, message: str) -> None:
if not message or not message.strip():
raise ValueError("Message must not be empty.")
- try:
- project = self.gitlab.projects.get(int(self.repository))
- mr = project.mergerequests.get(int(self.mr_iid))
- mr.notes.create({"body": message})
- except GitlabGetError as exc:
- raise RuntimeError(
- f"Failed to find project '{self.repository}' or merge request IID {self.mr_iid}."
- ) from exc
- except GitlabCreateError as exc:
- raise RuntimeError("Failed to create merge request note.") from exc
+ project = self.gitlab.projects.get(int(self.repository))
+ mr = project.mergerequests.get(int(self.mr_iid))
+ mr.notes.create({"body": message})
+ # try:
+
+ # except GitlabGetError as exc:
+ # raise RuntimeError(
+ # f"Failed to find project '{self.repository}' or merge request IID {self.mr_iid}."
+ # ) from exc
+ # except GitlabCreateError as exc:
+ # raise RuntimeError("Failed to create merge request note.") from exc
From ba40ab39d1fcc46243a836c38ba8fb14c0ed6520 Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 17:18:39 +0200
Subject: [PATCH 36/43] debug
---
codefox-template.yml | 3 +--
codefox/bots/gitlab_bot.py | 4 ++--
2 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/codefox-template.yml b/codefox-template.yml
index 4645853..1e13eee 100644
--- a/codefox-template.yml
+++ b/codefox-template.yml
@@ -20,8 +20,7 @@ codefox_review:
umask 077
{
echo "CODEFOX_API_KEY=$CODEFOX_API_KEY"
- echo "GITLAB_TOKEN=${GITLAB_TOKEN:-$CI_JOB_TOKEN}"
- echo "GITLAB_BOT=${GITLAB_TOKEN:-$CI_JOB_TOKEN}"
+ echo "GITLAB_TOKEN=${GITLAB_TOKEN}"
echo "GITLAB_URL=${CI_SERVER_URL}"
echo "GITLAB_REPOSITORY=${CI_PROJECT_ID}"
echo "PR_NUMBER=${CI_MERGE_REQUEST_IID}"
diff --git a/codefox/bots/gitlab_bot.py b/codefox/bots/gitlab_bot.py
index 5a7b614..cc36f11 100644
--- a/codefox/bots/gitlab_bot.py
+++ b/codefox/bots/gitlab_bot.py
@@ -7,14 +7,14 @@
class GitLabBot:
def __init__(self) -> None:
- self.gitlab_token = os.getenv("GITLAB_BOT")
+ self.gitlab_token = os.getenv("GITLAB_TOKEN")
self.gitlab_url = os.getenv("GITLAB_URL", "https://gitlab.com")
self.repository = os.getenv("GITLAB_REPOSITORY")
self.mr_iid = os.getenv("PR_NUMBER")
if not self.gitlab_token:
raise ValueError(
- "GITLAB_BOT environment variable is not set. "
+ "GITLAB_TOKEN environment variable is not set. "
"This token is required to authenticate with the GitLab API."
)
From b852a9367aacf5870edd2c2fc6cd9c66c375f928 Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 17:20:19 +0200
Subject: [PATCH 37/43] feat
---
codefox/bots/gitlab_bot.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/codefox/bots/gitlab_bot.py b/codefox/bots/gitlab_bot.py
index cc36f11..875331f 100644
--- a/codefox/bots/gitlab_bot.py
+++ b/codefox/bots/gitlab_bot.py
@@ -29,7 +29,7 @@ def __init__(self) -> None:
self.gitlab = Gitlab(
url=self.gitlab_url,
- job_token=self.gitlab_token,
+ private_token=self.gitlab_token,
)
def send(self, message: str) -> None:
From 5b8fa251ff518de23cb88eb6273d23dc9e1d88f7 Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 17:26:16 +0200
Subject: [PATCH 38/43] fix: improve error handling in GitLabBot for merge
request notes
---
codefox/bots/gitlab_bot.py | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/codefox/bots/gitlab_bot.py b/codefox/bots/gitlab_bot.py
index 875331f..ae1f29b 100644
--- a/codefox/bots/gitlab_bot.py
+++ b/codefox/bots/gitlab_bot.py
@@ -36,14 +36,14 @@ def send(self, message: str) -> None:
if not message or not message.strip():
raise ValueError("Message must not be empty.")
- project = self.gitlab.projects.get(int(self.repository))
- mr = project.mergerequests.get(int(self.mr_iid))
- mr.notes.create({"body": message})
- # try:
-
- # except GitlabGetError as exc:
- # raise RuntimeError(
- # f"Failed to find project '{self.repository}' or merge request IID {self.mr_iid}."
- # ) from exc
- # except GitlabCreateError as exc:
- # raise RuntimeError("Failed to create merge request note.") from exc
+
+ try:
+ project = self.gitlab.projects.get(int(self.repository))
+ mr = project.mergerequests.get(int(self.mr_iid))
+ mr.notes.create({"body": message})
+ except GitlabGetError as exc:
+ raise RuntimeError(
+ f"Failed to find project '{self.repository}' or merge request IID {self.mr_iid}."
+ ) from exc
+ except GitlabCreateError as exc:
+ raise RuntimeError("Failed to create merge request note.") from exc
From 91f7f732fb714b6ac9e7a19ffa73195caf37461d Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 23:03:25 +0200
Subject: [PATCH 39/43] debug
---
codefox/bots/gitlab_bot.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/codefox/bots/gitlab_bot.py b/codefox/bots/gitlab_bot.py
index ae1f29b..79c9cee 100644
--- a/codefox/bots/gitlab_bot.py
+++ b/codefox/bots/gitlab_bot.py
@@ -36,10 +36,10 @@ def send(self, message: str) -> None:
if not message or not message.strip():
raise ValueError("Message must not be empty.")
-
try:
project = self.gitlab.projects.get(int(self.repository))
mr = project.mergerequests.get(int(self.mr_iid))
+ print(message, self.mr_iid, self.repository)
mr.notes.create({"body": message})
except GitlabGetError as exc:
raise RuntimeError(
From b295fc798ac32d2b79f04a53ff4440a7f9a51efd Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 23:06:40 +0200
Subject: [PATCH 40/43] debug
---
codefox/cli/scan.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/codefox/cli/scan.py b/codefox/cli/scan.py
index 5c78d84..744d2d8 100644
--- a/codefox/cli/scan.py
+++ b/codefox/cli/scan.py
@@ -84,6 +84,7 @@ def _ci_response_answer(self, diff_text: str) -> None:
if self.github_bot is not None:
self.github_bot.send(response.text)
+ print(self.gitlab_bot)
if self.gitlab_bot is not None:
self.gitlab_bot.send(response.text)
From d15783d087bbaba4c902889b453f2b739c762190 Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 23:09:06 +0200
Subject: [PATCH 41/43] fix: change GITLAB_BOT to GITLAB_TOKEN
---
codefox/cli/scan.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/codefox/cli/scan.py b/codefox/cli/scan.py
index 744d2d8..e354512 100644
--- a/codefox/cli/scan.py
+++ b/codefox/cli/scan.py
@@ -24,7 +24,7 @@ def __init__(self, model: type[BaseAPI], args: dict[str, Any]):
self.gitlab_bot = None
if self.args.get("ci", False) and os.getenv("GITHUB_BOT"):
self.github_bot = GitHubBot()
- elif self.args.get("ci", False) and os.getenv("GITLAB_BOT"):
+ elif self.args.get("ci", False) and os.getenv("GITLAB_TOKEN"):
self.gitlab_bot = GitLabBot()
def execute(self) -> None:
From 1b6ba7664ffa6d3089ff2b82c1e57bb88fa0da92 Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 23:11:19 +0200
Subject: [PATCH 42/43] feat: removee debug
---
codefox/bots/gitlab_bot.py | 1 -
codefox/cli/scan.py | 3 +--
2 files changed, 1 insertion(+), 3 deletions(-)
diff --git a/codefox/bots/gitlab_bot.py b/codefox/bots/gitlab_bot.py
index 79c9cee..55bf4c4 100644
--- a/codefox/bots/gitlab_bot.py
+++ b/codefox/bots/gitlab_bot.py
@@ -39,7 +39,6 @@ def send(self, message: str) -> None:
try:
project = self.gitlab.projects.get(int(self.repository))
mr = project.mergerequests.get(int(self.mr_iid))
- print(message, self.mr_iid, self.repository)
mr.notes.create({"body": message})
except GitlabGetError as exc:
raise RuntimeError(
diff --git a/codefox/cli/scan.py b/codefox/cli/scan.py
index e354512..127c26a 100644
--- a/codefox/cli/scan.py
+++ b/codefox/cli/scan.py
@@ -83,8 +83,7 @@ def _ci_response_answer(self, diff_text: str) -> None:
response = self.model.execute(diff_text)
if self.github_bot is not None:
self.github_bot.send(response.text)
-
- print(self.gitlab_bot)
+
if self.gitlab_bot is not None:
self.gitlab_bot.send(response.text)
From fe434dc30084733f60ed2a87460527f13f90159e Mon Sep 17 00:00:00 2001
From: Timur Davydov
Date: Fri, 20 Mar 2026 23:26:13 +0200
Subject: [PATCH 43/43] chore:
---
codefox/bots/gitlab_bot.py | 18 ++++++++---
codefox/cli/index.py | 11 +++----
codefox/cli/init.py | 16 ++++++----
codefox/cli_manager.py | 4 +--
codefox/main.py | 18 ++++++-----
codefox/utils/local_rag.py | 2 +-
tests/test_gitlab_bot.py | 65 ++++++++++++++++++++++++++++++++++++++
tests/test_init.py | 38 ++++++++++++++++++++++
uv.lock | 29 ++++++++++++++++-
9 files changed, 170 insertions(+), 31 deletions(-)
create mode 100644 tests/test_gitlab_bot.py
create mode 100644 tests/test_init.py
diff --git a/codefox/bots/gitlab_bot.py b/codefox/bots/gitlab_bot.py
index 55bf4c4..8a3a01d 100644
--- a/codefox/bots/gitlab_bot.py
+++ b/codefox/bots/gitlab_bot.py
@@ -1,8 +1,7 @@
import os
-from urllib.parse import quote_plus
from gitlab import Gitlab
-from gitlab.exceptions import GitlabGetError, GitlabCreateError
+from gitlab.exceptions import GitlabCreateError, GitlabGetError
class GitLabBot:
@@ -36,13 +35,22 @@ def send(self, message: str) -> None:
if not message or not message.strip():
raise ValueError("Message must not be empty.")
+ repository = self.repository
+ mr_iid = self.mr_iid
+
+ if repository is None or mr_iid is None:
+ raise RuntimeError(
+ "GitLab project or merge request is not configured."
+ )
+
try:
- project = self.gitlab.projects.get(int(self.repository))
- mr = project.mergerequests.get(int(self.mr_iid))
+ project = self.gitlab.projects.get(int(repository))
+ mr = project.mergerequests.get(int(mr_iid))
mr.notes.create({"body": message})
except GitlabGetError as exc:
raise RuntimeError(
- f"Failed to find project '{self.repository}' or merge request IID {self.mr_iid}."
+ f"Failed to find project '{repository}' "
+ f"or merge request IID {mr_iid}."
) from exc
except GitlabCreateError as exc:
raise RuntimeError("Failed to create merge request note.") from exc
diff --git a/codefox/cli/index.py b/codefox/cli/index.py
index 8e35fc6..325c7aa 100644
--- a/codefox/cli/index.py
+++ b/codefox/cli/index.py
@@ -1,4 +1,5 @@
import os
+
from rich import print
from rich.markup import escape
@@ -14,12 +15,8 @@ def execute(self):
is_upload, error = self.model.upload_files(os.getcwd())
if not is_upload:
print(
- "[red]Failed to index files: "
- + escape(str(error))
- + "[/red]"
+ "[red]Failed to index files: " + escape(str(error)) + "[/red]"
)
return
-
- print(
- "[green]Successful to index files[/green]"
- )
+
+ print("[green]Successful to index files[/green]")
diff --git a/codefox/cli/init.py b/codefox/cli/init.py
index ac8971a..118ba79 100644
--- a/codefox/cli/init.py
+++ b/codefox/cli/init.py
@@ -1,5 +1,5 @@
-from typing import Any
from pathlib import Path
+from typing import Any
import yaml
from dotenv import load_dotenv, set_key
@@ -13,15 +13,17 @@
class Init(BaseCLI):
def __init__(self, args: dict[str, Any] | None = None):
- if not args.get("provider"):
+ resolved_args = args if args is not None else {}
+
+ if not resolved_args.get("provider"):
self.model_enum = self._ask_model()
else:
- self.model_enum = ModelEnum.by_name(args["provider"])
-
+ self.model_enum = ModelEnum.by_name(resolved_args["provider"])
+
self.api_class: type[BaseAPI] = self.model_enum.api_class
-
- self.args = args
-
+
+ self.args: dict[str, Any] = resolved_args
+
self.config_path = Path(".codefoxenv")
self.ignore_path = Path(".codefoxignore")
self.yaml_config_path = Path(".codefox.yml")
diff --git a/codefox/cli_manager.py b/codefox/cli_manager.py
index b53fa91..946d28e 100644
--- a/codefox/cli_manager.py
+++ b/codefox/cli_manager.py
@@ -8,10 +8,10 @@
from codefox.api.base_api import BaseAPI
from codefox.api.model_enum import ModelEnum
from codefox.cli.clean import Clean
+from codefox.cli.index import Index
from codefox.cli.init import Init
from codefox.cli.list import List
from codefox.cli.scan import Scan
-from codefox.cli.index import Index
from codefox.utils.helper import Helper
@@ -35,7 +35,7 @@ def run(self) -> None:
version = importlib.metadata.version("codefox")
print(f"[green]CodeFox CLI version {version}[/green]")
return
-
+
if self.command == "index":
api_class = self._get_api_class()
index = Index(api_class)
diff --git a/codefox/main.py b/codefox/main.py
index e1f0db6..e7c1fa5 100755
--- a/codefox/main.py
+++ b/codefox/main.py
@@ -23,31 +23,33 @@ def scan(
)
manager.run()
+
@app.command("index")
def index():
"""Index files"""
- CLIManager(
- command="index", args={}
- ).run()
+ CLIManager(command="index", args={}).run()
+
@app.command("init")
def init(
- provider: str | None = typer.Option(None, help="Provider."),
- token: str | None = typer.Option(None, help="Token provider.")
+ provider: str | None = typer.Option(None, help="Provider."),
+ token: str | None = typer.Option(None, help="Token provider."),
):
"""Initialize CodeFox."""
manager = CLIManager(
- command="init",
+ command="init",
args={
"provider": provider,
"token": token,
- }
+ },
)
manager.run()
@app.command("list")
-def list_models(type_model: str = typer.Argument("models", help="Model type.")):
+def list_models(
+ type_model: str = typer.Argument("models", help="Model type."),
+):
"""List available models."""
manager = CLIManager(
command="list",
diff --git a/codefox/utils/local_rag.py b/codefox/utils/local_rag.py
index dcdde31..d6ca44f 100644
--- a/codefox/utils/local_rag.py
+++ b/codefox/utils/local_rag.py
@@ -194,7 +194,7 @@ def build(self) -> None:
self.console.print(
"[magenta]Building Qdrant vector index...[/magenta]"
)
-
+
dim: int | None = None
for i in track(
range(0, len(texts), batch_size),
diff --git a/tests/test_gitlab_bot.py b/tests/test_gitlab_bot.py
new file mode 100644
index 0000000..c12de5c
--- /dev/null
+++ b/tests/test_gitlab_bot.py
@@ -0,0 +1,65 @@
+"""Tests for GitLab bot."""
+
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from codefox.bots.gitlab_bot import GitLabBot
+
+
+def test_send_creates_merge_request_note(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.setenv("GITLAB_TOKEN", "token")
+ monkeypatch.setenv("GITLAB_REPOSITORY", "123")
+ monkeypatch.setenv("PR_NUMBER", "456")
+
+ with patch("codefox.bots.gitlab_bot.Gitlab") as mock_gitlab_class:
+ mock_project = MagicMock()
+ mock_mr = MagicMock()
+ mock_gitlab = mock_gitlab_class.return_value
+ mock_gitlab.projects.get.return_value = mock_project
+ mock_project.mergerequests.get.return_value = mock_mr
+
+ bot = GitLabBot()
+ bot.send("hello")
+
+ mock_gitlab_class.assert_called_once_with(
+ url="https://gitlab.com",
+ private_token="token",
+ )
+ mock_gitlab.projects.get.assert_called_once_with(123)
+ mock_project.mergerequests.get.assert_called_once_with(456)
+ mock_mr.notes.create.assert_called_once_with({"body": "hello"})
+
+
+def test_send_rejects_empty_message(monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.setenv("GITLAB_TOKEN", "token")
+ monkeypatch.setenv("GITLAB_REPOSITORY", "123")
+ monkeypatch.setenv("PR_NUMBER", "456")
+
+ with patch("codefox.bots.gitlab_bot.Gitlab"):
+ bot = GitLabBot()
+
+ with pytest.raises(ValueError, match="Message must not be empty"):
+ bot.send(" ")
+
+
+def test_send_raises_when_identifiers_are_not_configured(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.setenv("GITLAB_TOKEN", "token")
+ monkeypatch.setenv("GITLAB_REPOSITORY", "123")
+ monkeypatch.setenv("PR_NUMBER", "456")
+
+ with patch("codefox.bots.gitlab_bot.Gitlab") as mock_gitlab_class:
+ bot = GitLabBot()
+ bot.repository = None
+
+ with pytest.raises(
+ RuntimeError,
+ match="GitLab project or merge request is not configured",
+ ):
+ bot.send("hello")
+
+ mock_gitlab_class.return_value.projects.get.assert_not_called()
diff --git a/tests/test_init.py b/tests/test_init.py
new file mode 100644
index 0000000..2b3cb78
--- /dev/null
+++ b/tests/test_init.py
@@ -0,0 +1,38 @@
+"""Tests for Init command."""
+
+from unittest.mock import patch
+
+from codefox.api.model_enum import ModelEnum
+from codefox.cli.init import Init
+
+
+def test_init_normalizes_none_args() -> None:
+ with patch.object(Init, "_ask_model", return_value=ModelEnum.GEMINI):
+ init = Init()
+
+ assert init.model_enum is ModelEnum.GEMINI
+ assert init.api_class is ModelEnum.GEMINI.api_class
+ assert init.args == {}
+
+
+def test_execute_uses_provider_and_token_from_args() -> None:
+ init = Init({"provider": "gemini", "token": "test-token"})
+
+ with (
+ patch.object(init, "_ask_api_key") as mock_ask_api_key,
+ patch.object(init, "_write_config", return_value=True) as mock_write,
+ patch.object(init, "_ensure_ignore_file") as mock_ignore,
+ patch.object(init, "_ensure_yaml_config") as mock_yaml,
+ patch.object(init, "_ensure_gitignore") as mock_gitignore,
+ patch.object(
+ init, "_check_connection", return_value=True
+ ) as mock_check,
+ ):
+ init.execute()
+
+ mock_ask_api_key.assert_not_called()
+ mock_write.assert_called_once_with("test-token")
+ mock_ignore.assert_called_once()
+ mock_yaml.assert_called_once()
+ mock_gitignore.assert_called_once()
+ mock_check.assert_called_once()
diff --git a/uv.lock b/uv.lock
index 0d83e73..7c6871b 100644
--- a/uv.lock
+++ b/uv.lock
@@ -217,7 +217,7 @@ wheels = [
[[package]]
name = "codefox"
-version = "0.4.0"
+version = "0.4.1"
source = { editable = "." }
dependencies = [
{ name = "bm25s" },
@@ -232,6 +232,7 @@ dependencies = [
{ name = "pygithub" },
{ name = "pygments" },
{ name = "python-dotenv" },
+ { name = "python-gitlab" },
{ name = "pyyaml" },
{ name = "qdrant-client" },
{ name = "requests" },
@@ -267,6 +268,7 @@ requires-dist = [
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" },
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0" },
{ name = "python-dotenv", specifier = "==1.2.1" },
+ { name = "python-gitlab", specifier = "==8.1.0" },
{ name = "pyyaml", specifier = "==6.0.3" },
{ name = "qdrant-client", specifier = ">=1.7.0" },
{ name = "requests", specifier = ">=2.28.0" },
@@ -1742,6 +1744,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
]
+[[package]]
+name = "python-gitlab"
+version = "8.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "requests" },
+ { name = "requests-toolbelt" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/20/1d/a62fea1f3312fd9e58af41466ae072796a09684dd0cd825cc042ba39488c/python_gitlab-8.1.0.tar.gz", hash = "sha256:660f15e3f889ec430797d260322bc61d90f8d90accfc10ba37593b11aed371bd", size = 401576, upload-time = "2026-02-28T01:26:32.757Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/d4/9848be62ef23fcac203f4386faf43a2cc13a4888447b3f5fbf7346f31374/python_gitlab-8.1.0-py3-none-any.whl", hash = "sha256:b1a59e81e5e0363185b446a707dc92c27ee8bf1fc14ce75ed8eafa58cbdce63a", size = 144498, upload-time = "2026-02-28T01:26:31.14Z" },
+]
+
[[package]]
name = "pywin32"
version = "311"
@@ -1953,6 +1968,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
]
+[[package]]
+name = "requests-toolbelt"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" },
+]
+
[[package]]
name = "rich"
version = "14.3.2"