From 96a5b1764491772d6c48d13562f379d7d5f91ce8 Mon Sep 17 00:00:00 2001 From: Levente Polyak Date: Tue, 24 Mar 2026 19:43:24 +0100 Subject: [PATCH 1/8] feat(ai): generalize run_cli function into utils helper for reuse This preparation unifies the claud_cli and custom_cmd invocation into a general purpose utils runner that can be reused. This will allow future cli integrations to be done simpler without code duplication. --- lua/codereview/ai/providers/claude_cli.lua | 68 +------------------ lua/codereview/ai/providers/custom_cmd.lua | 70 +------------------- lua/codereview/ai/providers/utils.lua | 77 ++++++++++++++++++++++ 3 files changed, 82 insertions(+), 133 deletions(-) create mode 100644 lua/codereview/ai/providers/utils.lua diff --git a/lua/codereview/ai/providers/claude_cli.lua b/lua/codereview/ai/providers/claude_cli.lua index 8552bad..bf20a6b 100644 --- a/lua/codereview/ai/providers/claude_cli.lua +++ b/lua/codereview/ai/providers/claude_cli.lua @@ -1,5 +1,5 @@ local config = require("codereview.config") -local log = require("codereview.log") +local utils = require("codereview.ai.providers.utils") local M = {} function M.build_cmd(claude_cmd, agent) @@ -23,71 +23,7 @@ function M.run(prompt, callback, opts) local claude_cmd = pcfg.cmd or cfg.ai.claude_cmd or "claude" local agent = (not opts.skip_agent) and (pcfg.agent or cfg.ai.agent) or nil local cmd = M.build_cmd(claude_cmd, agent) - log.debug("AI claude_cli: starting " .. table.concat(cmd, " ")) - local stdout_chunks = {} - local stderr_chunks = {} - local done = false - - local job_id = vim.fn.jobstart(cmd, { - stdout_buffered = true, - stderr_buffered = true, - on_stdout = function(_, data) - if data then - for i, chunk in ipairs(data) do - if i < #data or chunk ~= "" then - table.insert(stdout_chunks, chunk) - end - end - end - end, - on_stderr = function(_, data) - if data then - for i, chunk in ipairs(data) do - if i < #data or chunk ~= "" then - table.insert(stderr_chunks, chunk) - end - end - end - end, - on_exit = function(_, code) - if done then - return - end - done = true - vim.schedule(function() - local stderr_str = table.concat(stderr_chunks, "\n") - if stderr_str ~= "" then - log.warn("AI claude_cli stderr: " .. stderr_str) - end - local output = table.concat(stdout_chunks, "\n") - if code ~= 0 then - local msg = "Claude CLI exited with code " .. code - if output ~= "" then - msg = msg .. "\n" .. output - end - if stderr_str ~= "" then - msg = msg .. "\n" .. stderr_str - end - log.error("AI claude_cli: " .. msg) - callback(nil, msg) - else - log.debug("AI claude_cli: completed, output length=" .. #output) - callback(output) - end - end) - end, - }) - - if job_id <= 0 then - local msg = "Failed to start Claude CLI. Is '" .. claude_cmd .. "' in your PATH?" - log.error("AI claude_cli: " .. msg) - callback(nil, msg) - return - end - - vim.fn.chansend(job_id, prompt) - vim.fn.chanclose(job_id, "stdin") - return job_id + return utils.run_cli(prompt, callback, cmd) end return M diff --git a/lua/codereview/ai/providers/custom_cmd.lua b/lua/codereview/ai/providers/custom_cmd.lua index 0e0dc72..8be5df8 100644 --- a/lua/codereview/ai/providers/custom_cmd.lua +++ b/lua/codereview/ai/providers/custom_cmd.lua @@ -1,5 +1,5 @@ local config = require("codereview.config") -local log = require("codereview.log") +local utils = require("codereview.ai.providers.utils") local M = {} function M.build_cmd(cmd, args) @@ -10,7 +10,7 @@ function M.build_cmd(cmd, args) return t end -function M.run(prompt, callback, opts) +function M.run(prompt, callback) local cfg = config.get() if not cfg.ai.enabled then callback(nil, "AI review is disabled in config") @@ -24,71 +24,7 @@ function M.run(prompt, callback, opts) end local cmd = M.build_cmd(pcfg.cmd, pcfg.args) - log.debug("AI custom_cmd: starting " .. table.concat(cmd, " ")) - local stdout_chunks = {} - local stderr_chunks = {} - local done = false - - local job_id = vim.fn.jobstart(cmd, { - stdout_buffered = true, - stderr_buffered = true, - on_stdout = function(_, data) - if data then - for i, chunk in ipairs(data) do - if i < #data or chunk ~= "" then - table.insert(stdout_chunks, chunk) - end - end - end - end, - on_stderr = function(_, data) - if data then - for i, chunk in ipairs(data) do - if i < #data or chunk ~= "" then - table.insert(stderr_chunks, chunk) - end - end - end - end, - on_exit = function(_, code) - if done then - return - end - done = true - vim.schedule(function() - local stderr_str = table.concat(stderr_chunks, "\n") - if stderr_str ~= "" then - log.warn("AI custom_cmd stderr: " .. stderr_str) - end - local output = table.concat(stdout_chunks, "\n") - if code ~= 0 then - local msg = "Custom command exited with code " .. code - if output ~= "" then - msg = msg .. "\n" .. output - end - if stderr_str ~= "" then - msg = msg .. "\n" .. stderr_str - end - log.error("AI custom_cmd: " .. msg) - callback(nil, msg) - else - log.debug("AI custom_cmd: completed, output length=" .. #output) - callback(output) - end - end) - end, - }) - - if job_id <= 0 then - local msg = "Failed to start '" .. pcfg.cmd .. "'. Is it in your PATH?" - log.error("AI custom_cmd: " .. msg) - callback(nil, msg) - return - end - - vim.fn.chansend(job_id, prompt) - vim.fn.chanclose(job_id, "stdin") - return job_id + return utils.run_cli(prompt, callback, cmd) end return M diff --git a/lua/codereview/ai/providers/utils.lua b/lua/codereview/ai/providers/utils.lua new file mode 100644 index 0000000..7f549fc --- /dev/null +++ b/lua/codereview/ai/providers/utils.lua @@ -0,0 +1,77 @@ +local log = require("codereview.log") +local M = {} + +---Run a CLI command asynchronously, sending the prompt via stdin and collecting output. +---@param prompt string|string[] prompt that will be passed to the AI +---@param callback fun(string?, string?) callback that takes either the AI output as first argument or an error message as second argument. +---@param cmd string[] command to execute that consumes the prompt input to produce an output passed to the callback. +---@return integer Returns |job-id| on success, 0 on invalid arguments (or job table is full), -1 if {cmd}[0] or 'shell' is not executable. +function M.run_cli(prompt, callback, cmd) + log.debug("AI cmd: starting " .. table.concat(cmd, " ")) + local stdout_chunks = {} + local stderr_chunks = {} + local done = false + + local job_id = vim.fn.jobstart(cmd, { + stdout_buffered = true, + stderr_buffered = true, + on_stdout = function(_, data) + if data then + for i, chunk in ipairs(data) do + if i < #data or chunk ~= "" then + table.insert(stdout_chunks, chunk) + end + end + end + end, + on_stderr = function(_, data) + if data then + for i, chunk in ipairs(data) do + if i < #data or chunk ~= "" then + table.insert(stderr_chunks, chunk) + end + end + end + end, + on_exit = function(_, code) + if done then + return + end + done = true + vim.schedule(function() + local stderr_str = table.concat(stderr_chunks, "\n") + if stderr_str ~= "" then + log.warn("AI cmd stderr: " .. stderr_str) + end + local output = table.concat(stdout_chunks, "\n") + if code ~= 0 then + local msg = "Command exited with code " .. code + if output ~= "" then + msg = msg .. "\n" .. output + end + if stderr_str ~= "" then + msg = msg .. "\n" .. stderr_str + end + log.error("AI cmd: " .. msg) + callback(nil, msg) + else + log.debug("AI cmd: completed, output length=" .. #output) + callback(output) + end + end) + end, + }) + + if job_id <= 0 then + local msg = "Failed to start '" .. table.concat(cmd, " ") .. "'. Is it in your PATH?" + log.error("AI cmd: " .. msg) + callback(nil, msg) + return job_id + end + + vim.fn.chansend(job_id, prompt) + vim.fn.chanclose(job_id, "stdin") + return job_id +end + +return M From 5dde5075d3debb5ace687692afe3bc26553be4cc Mon Sep 17 00:00:00 2001 From: Levente Polyak Date: Tue, 24 Mar 2026 19:47:46 +0100 Subject: [PATCH 2/8] feat(ai): Add OpenAI Codex CLI integration --- README.md | 4 ++- doc/codereview.txt | 25 ++++++++++++++++- lua/codereview/ai/providers/codex_cli.lua | 28 +++++++++++++++++++ lua/codereview/ai/providers/init.lua | 1 + lua/codereview/config.lua | 17 +++++++++-- .../ai/providers/codex_cli_spec.lua | 22 +++++++++++++++ 6 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 lua/codereview/ai/providers/codex_cli.lua create mode 100644 tests/codereview/ai/providers/codex_cli_spec.lua diff --git a/README.md b/README.md index 42072a5..06aaeae 100644 --- a/README.md +++ b/README.md @@ -159,11 +159,12 @@ require("codereview").setup({ -- AI review ai = { enabled = true, - provider = "claude_cli", -- "claude_cli" | "anthropic" | "openai" | "ollama" | "custom_cmd" + provider = "claude_cli", -- "claude_cli" | "codex_cli" | "anthropic" | "openai" | "ollama" | "custom_cmd" review_level = "info", -- "info" | "suggestion" | "warning" | "error" max_file_size = 500, -- skip files larger than N lines (0 = unlimited) claude_cli = { cmd = "claude", agent = "code-review" }, + codex_cli = { cmd = "codex", model = nil }, anthropic = { api_key = nil, model = "claude-sonnet-4-20250514" }, openai = { api_key = nil, model = "gpt-4o", base_url = nil }, ollama = { model = "llama3", base_url = "http://localhost:11434" }, @@ -185,6 +186,7 @@ Set `ai.provider` to choose a backend. Each provider has its own sub-table: | Provider | Config key | Requirements | |----------|-----------|--------------| | Claude CLI | `claude_cli` | [Claude CLI](https://docs.anthropic.com/en/docs/claude-code) installed | +| Codex CLI | `codex_cli` | [Codex CLI](https://github.com/openai/codex) installed and authenticated | | Anthropic API | `anthropic` | `api_key` set | | OpenAI | `openai` | `api_key` set | | Ollama | `ollama` | Ollama running locally | diff --git a/doc/codereview.txt b/doc/codereview.txt index ef3e1bd..06de111 100644 --- a/doc/codereview.txt +++ b/doc/codereview.txt @@ -306,7 +306,7 @@ Options for AI review. See |codereview-ai-review|. -- boolean enable AI review features enabled = true, - -- "claude_cli" | "anthropic" | "openai" | "ollama" | "custom_cmd" + -- "claude_cli" | "codex_cli" | "anthropic" | "openai" | "ollama" | "custom_cmd" provider = "claude_cli", -- "info" | "suggestion" | "warning" | "error" minimum severity to display @@ -321,6 +321,10 @@ Options for AI review. See |codereview-ai-review|. cmd = "claude", -- path or name of the claude binary agent = "code-review", -- Claude agent/profile name }, + codex_cli = { + cmd = "codex", -- path or name of the codex binary + model = nil, -- optional model override + }, anthropic = { api_key = nil, -- falls back to ANTHROPIC_API_KEY model = "claude-sonnet-4-20250514", @@ -380,6 +384,25 @@ Requirements: } < +------------------------------------------------------------------------------ +Codex CLI *codereview-ai-codex-cli* + +Uses the `codex exec` command-line interface to prompt non-interactively using +the currently configured or default model until explicitly specified. + +Requirements: + - `codex` binary installed and on PATH + - Codex CLI already authenticated +>lua + ai = { + provider = "codex_cli", + codex_cli = { + cmd = "codex", -- binary name or full path + model = "gpt-5.4", -- optional model override + }, + } +< + ------------------------------------------------------------------------------ Anthropic API *codereview-ai-anthropic* diff --git a/lua/codereview/ai/providers/codex_cli.lua b/lua/codereview/ai/providers/codex_cli.lua new file mode 100644 index 0000000..5a6deb0 --- /dev/null +++ b/lua/codereview/ai/providers/codex_cli.lua @@ -0,0 +1,28 @@ +local config = require("codereview.config") +local utils = require("codereview.ai.providers.utils") +local M = {} + +function M.build_cmd(codex_cmd, model) + local cmd = { codex_cmd or "codex", "exec" } + table.insert(cmd, "--sandbox=read-only") + if model and model ~= "" then + table.insert(cmd, "--model") + table.insert(cmd, model) + end + table.insert(cmd, "--") + return cmd +end + +function M.run(prompt, callback) + local cfg = config.get() + if not cfg.ai.enabled then + callback(nil, "AI review is disabled in config") + return + end + + local pcfg = cfg.ai.codex_cli or {} + local cmd = M.build_cmd(pcfg.cmd, pcfg.model) + return utils.run_cli(prompt, callback, cmd) +end + +return M diff --git a/lua/codereview/ai/providers/init.lua b/lua/codereview/ai/providers/init.lua index 14307d3..f2ecf88 100644 --- a/lua/codereview/ai/providers/init.lua +++ b/lua/codereview/ai/providers/init.lua @@ -2,6 +2,7 @@ local M = {} local valid_providers = { claude_cli = "codereview.ai.providers.claude_cli", + codex_cli = "codereview.ai.providers.codex_cli", anthropic = "codereview.ai.providers.anthropic", openai = "codereview.ai.providers.openai", ollama = "codereview.ai.providers.ollama", diff --git a/lua/codereview/config.lua b/lua/codereview/config.lua index 242d2b9..68c5d01 100644 --- a/lua/codereview/config.lua +++ b/lua/codereview/config.lua @@ -27,10 +27,11 @@ ---@class codereview.config.AI ---@field enabled? boolean enable AI Review (default: `true`) ----@field provider? "claude_cli"|"anthropic"|"openai"|"ollama"|"custom_cmd" AI Provider to use +---@field provider? "claude_cli"|"codex_cli"|"anthropic"|"openai"|"ollama"|"custom_cmd" AI Provider to use ---@field review_level? "info"|"suggestion"|"warning"|"error" controls the verbosity of AI code reviews (default: `info`) ---@field max_file_size? integer skip files larger than N lines (0 = unlimited) (default: 500) ---@field claude_cli? codereview.config.ai.ClaudeCli Claude CLI options +---@field codex_cli? codereview.config.ai.CodexCLI Codex CLI options ---@field anthropic? codereview.config.ai.Anthropic Anthropic API options ---@field openai? codereview.config.ai.OpenAI OpenAI API options ---@field ollama? codereview.config.ai.Ollama Ollama options @@ -40,6 +41,10 @@ ---@field cmd? string Claude CLI command (default: `claude_cli`) ---@field agent? string Claude Agent (default: `code-review`) +---@class codereview.config.ai.CodexCLI +---@field cmd? string Codex CLI command (default: `codex`) +---@field model? string Codex model name + ---@class codereview.config.ai.Anthropic ---@field api_key? string Anthropic API key ---@field model? string Anthropic model name @@ -77,6 +82,7 @@ local defaults = { review_level = "info", max_file_size = 500, claude_cli = { cmd = "claude", agent = "code-review" }, + codex_cli = { cmd = "codex", model = nil }, anthropic = { api_key = nil, model = "claude-sonnet-4-20250514" }, openai = { api_key = nil, model = "gpt-4o", base_url = nil }, ollama = { model = "llama3", base_url = "http://localhost:11434" }, @@ -112,7 +118,14 @@ local function validate(c) c.ai.review_level = "info" end c.ai.max_file_size = math.max(0, c.ai.max_file_size or 500) - local valid_providers = { claude_cli = true, anthropic = true, openai = true, ollama = true, custom_cmd = true } + local valid_providers = { + claude_cli = true, + codex_cli = true, + anthropic = true, + openai = true, + ollama = true, + custom_cmd = true, + } if not valid_providers[c.ai.provider] then c.ai.provider = "claude_cli" end diff --git a/tests/codereview/ai/providers/codex_cli_spec.lua b/tests/codereview/ai/providers/codex_cli_spec.lua new file mode 100644 index 0000000..433964a --- /dev/null +++ b/tests/codereview/ai/providers/codex_cli_spec.lua @@ -0,0 +1,22 @@ +package.loaded["codereview.config"] = { + get = function() + return { ai = { enabled = true, provider = "codex_cli", codex_cli = { cmd = "codex", model = nil } } } + end, +} +package.loaded["codereview.log"] = { debug = function() end, warn = function() end, error = function() end } + +local codex_cli = require("codereview.ai.providers.codex_cli") + +describe("ai.providers.codex_cli", function() + describe("build_cmd", function() + it("builds default codex command", function() + assert.same({ "codex", "exec", "--sandbox=read-only", "--" }, codex_cli.build_cmd("codex")) + end) + it("includes model flag when provided", function() + assert.same( + { "codex", "exec", "--sandbox=read-only", "--model", "model", "--" }, + codex_cli.build_cmd("codex", "model") + ) + end) + end) +end) From 193531b9fe841ebfead6d0f445872b880b50b2f9 Mon Sep 17 00:00:00 2001 From: Levente Polyak Date: Tue, 24 Mar 2026 20:58:06 +0100 Subject: [PATCH 3/8] feat(ai): Add Google Gemini CLI integration --- README.md | 4 ++- doc/codereview.txt | 25 ++++++++++++++++- lua/codereview/ai/providers/gemini_cli.lua | 27 +++++++++++++++++++ lua/codereview/ai/providers/init.lua | 1 + lua/codereview/config.lua | 9 ++++++- .../ai/providers/gemini_cli_spec.lua | 21 +++++++++++++++ 6 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 lua/codereview/ai/providers/gemini_cli.lua create mode 100644 tests/codereview/ai/providers/gemini_cli_spec.lua diff --git a/README.md b/README.md index 06aaeae..6be7cd7 100644 --- a/README.md +++ b/README.md @@ -159,12 +159,13 @@ require("codereview").setup({ -- AI review ai = { enabled = true, - provider = "claude_cli", -- "claude_cli" | "codex_cli" | "anthropic" | "openai" | "ollama" | "custom_cmd" + provider = "claude_cli", -- "claude_cli" | "codex_cli" | "gemini_cli" | "anthropic" | "openai" | "ollama" | "custom_cmd" review_level = "info", -- "info" | "suggestion" | "warning" | "error" max_file_size = 500, -- skip files larger than N lines (0 = unlimited) claude_cli = { cmd = "claude", agent = "code-review" }, codex_cli = { cmd = "codex", model = nil }, + gemini_cli = { cmd = "gemini", model = nil }, anthropic = { api_key = nil, model = "claude-sonnet-4-20250514" }, openai = { api_key = nil, model = "gpt-4o", base_url = nil }, ollama = { model = "llama3", base_url = "http://localhost:11434" }, @@ -187,6 +188,7 @@ Set `ai.provider` to choose a backend. Each provider has its own sub-table: |----------|-----------|--------------| | Claude CLI | `claude_cli` | [Claude CLI](https://docs.anthropic.com/en/docs/claude-code) installed | | Codex CLI | `codex_cli` | [Codex CLI](https://github.com/openai/codex) installed and authenticated | +| Gemini CLI | `gemini_cli` | [Gemini CLI](https://github.com/google-gemini/gemini-cli) installed and authenticated | | Anthropic API | `anthropic` | `api_key` set | | OpenAI | `openai` | `api_key` set | | Ollama | `ollama` | Ollama running locally | diff --git a/doc/codereview.txt b/doc/codereview.txt index 06de111..bfbbee0 100644 --- a/doc/codereview.txt +++ b/doc/codereview.txt @@ -306,7 +306,7 @@ Options for AI review. See |codereview-ai-review|. -- boolean enable AI review features enabled = true, - -- "claude_cli" | "codex_cli" | "anthropic" | "openai" | "ollama" | "custom_cmd" + -- "claude_cli" | "codex_cli" | "gemini_cli" | "anthropic" | "openai" | "ollama" | "custom_cmd" provider = "claude_cli", -- "info" | "suggestion" | "warning" | "error" minimum severity to display @@ -325,6 +325,10 @@ Options for AI review. See |codereview-ai-review|. cmd = "codex", -- path or name of the codex binary model = nil, -- optional model override }, + gemini_cli = { + cmd = "gemini", -- path or name of the gemini binary + model = nil, -- optional model override + }, anthropic = { api_key = nil, -- falls back to ANTHROPIC_API_KEY model = "claude-sonnet-4-20250514", @@ -403,6 +407,25 @@ Requirements: } < +------------------------------------------------------------------------------ +Gemini CLI *codereview-ai-gemini-cli* + +Uses the `gemini` command-line interface to prompt non-interactively using +the currently configured or default model until explicitly specified. + +Requirements: + - `gemini` binary installed and on PATH + - Gemini CLI already authenticated +>lua + ai = { + provider = "gemini_cli", + gemini_cli = { + cmd = "gemini", -- binary name or full path + model = "gemini-3-pro", -- optional model override + }, + } +< + ------------------------------------------------------------------------------ Anthropic API *codereview-ai-anthropic* diff --git a/lua/codereview/ai/providers/gemini_cli.lua b/lua/codereview/ai/providers/gemini_cli.lua new file mode 100644 index 0000000..beffe03 --- /dev/null +++ b/lua/codereview/ai/providers/gemini_cli.lua @@ -0,0 +1,27 @@ +local config = require("codereview.config") +local utils = require("codereview.ai.providers.utils") +local M = {} + +function M.build_cmd(gemini_cmd, model) + local cmd = { gemini_cmd or "gemini" } + table.insert(cmd, "--approval-mode=plan") + if model and model ~= "" then + table.insert(cmd, "--model") + table.insert(cmd, model) + end + return cmd +end + +function M.run(prompt, callback) + local cfg = config.get() + if not cfg.ai.enabled then + callback(nil, "AI review is disabled in config") + return + end + + local pcfg = cfg.ai.gemini_cli or {} + local cmd = M.build_cmd(pcfg.cmd, pcfg.model) + return utils.run_cli(prompt, callback, cmd) +end + +return M diff --git a/lua/codereview/ai/providers/init.lua b/lua/codereview/ai/providers/init.lua index f2ecf88..9bd244a 100644 --- a/lua/codereview/ai/providers/init.lua +++ b/lua/codereview/ai/providers/init.lua @@ -3,6 +3,7 @@ local M = {} local valid_providers = { claude_cli = "codereview.ai.providers.claude_cli", codex_cli = "codereview.ai.providers.codex_cli", + gemini_cli = "codereview.ai.providers.gemini_cli", anthropic = "codereview.ai.providers.anthropic", openai = "codereview.ai.providers.openai", ollama = "codereview.ai.providers.ollama", diff --git a/lua/codereview/config.lua b/lua/codereview/config.lua index 68c5d01..4662c9f 100644 --- a/lua/codereview/config.lua +++ b/lua/codereview/config.lua @@ -27,11 +27,12 @@ ---@class codereview.config.AI ---@field enabled? boolean enable AI Review (default: `true`) ----@field provider? "claude_cli"|"codex_cli"|"anthropic"|"openai"|"ollama"|"custom_cmd" AI Provider to use +---@field provider? "claude_cli"|"codex_cli"|"gemini_cli"|"anthropic"|"openai"|"ollama"|"custom_cmd" AI Provider to use ---@field review_level? "info"|"suggestion"|"warning"|"error" controls the verbosity of AI code reviews (default: `info`) ---@field max_file_size? integer skip files larger than N lines (0 = unlimited) (default: 500) ---@field claude_cli? codereview.config.ai.ClaudeCli Claude CLI options ---@field codex_cli? codereview.config.ai.CodexCLI Codex CLI options +---@field gemini_cli? codereview.config.ai.GeminiCLI Gemini CLI options ---@field anthropic? codereview.config.ai.Anthropic Anthropic API options ---@field openai? codereview.config.ai.OpenAI OpenAI API options ---@field ollama? codereview.config.ai.Ollama Ollama options @@ -45,6 +46,10 @@ ---@field cmd? string Codex CLI command (default: `codex`) ---@field model? string Codex model name +---@class codereview.config.ai.GeminiCLI +---@field cmd? string Gemini CLI command (default: `gemini`) +---@field model? string Gemini model name + ---@class codereview.config.ai.Anthropic ---@field api_key? string Anthropic API key ---@field model? string Anthropic model name @@ -83,6 +88,7 @@ local defaults = { max_file_size = 500, claude_cli = { cmd = "claude", agent = "code-review" }, codex_cli = { cmd = "codex", model = nil }, + gemini_cli = { cmd = "gemini", model = nil }, anthropic = { api_key = nil, model = "claude-sonnet-4-20250514" }, openai = { api_key = nil, model = "gpt-4o", base_url = nil }, ollama = { model = "llama3", base_url = "http://localhost:11434" }, @@ -121,6 +127,7 @@ local function validate(c) local valid_providers = { claude_cli = true, codex_cli = true, + gemini_cli = true, anthropic = true, openai = true, ollama = true, diff --git a/tests/codereview/ai/providers/gemini_cli_spec.lua b/tests/codereview/ai/providers/gemini_cli_spec.lua new file mode 100644 index 0000000..03a99b3 --- /dev/null +++ b/tests/codereview/ai/providers/gemini_cli_spec.lua @@ -0,0 +1,21 @@ +package.loaded["codereview.config"] = { + get = function() + return { + ai = { enabled = true, provider = "gemini_cli", gemini_cli = { cmd = "gemini", model = nil } }, + } + end, +} +package.loaded["codereview.log"] = { debug = function() end, warn = function() end, error = function() end } + +local gemini_cli = require("codereview.ai.providers.gemini_cli") + +describe("ai.providers.gemini_cli", function() + describe("build_cmd", function() + it("builds default gemini command", function() + assert.same({ "gemini", "--approval-mode=plan" }, gemini_cli.build_cmd("gemini")) + end) + it("includes model flag when provided", function() + assert.same({ "gemini", "--approval-mode=plan", "--model", "model" }, gemini_cli.build_cmd("gemini", "model")) + end) + end) +end) From 660fcbfdc43a05a2059270f124a3e9e6d7c90fa2 Mon Sep 17 00:00:00 2001 From: Levente Polyak Date: Tue, 24 Mar 2026 21:01:54 +0100 Subject: [PATCH 4/8] feat(ai): Add Alibaba Qwen CLI integration --- README.md | 4 ++- doc/codereview.txt | 25 ++++++++++++++++- lua/codereview/ai/providers/init.lua | 1 + lua/codereview/ai/providers/qwen_cli.lua | 27 +++++++++++++++++++ lua/codereview/config.lua | 9 ++++++- .../codereview/ai/providers/qwen_cli_spec.lua | 21 +++++++++++++++ 6 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 lua/codereview/ai/providers/qwen_cli.lua create mode 100644 tests/codereview/ai/providers/qwen_cli_spec.lua diff --git a/README.md b/README.md index 6be7cd7..e133906 100644 --- a/README.md +++ b/README.md @@ -159,13 +159,14 @@ require("codereview").setup({ -- AI review ai = { enabled = true, - provider = "claude_cli", -- "claude_cli" | "codex_cli" | "gemini_cli" | "anthropic" | "openai" | "ollama" | "custom_cmd" + provider = "claude_cli", -- "claude_cli" | "codex_cli" | "gemini_cli" | "qwen_cli" | "anthropic" | "openai" | "ollama" | "custom_cmd" review_level = "info", -- "info" | "suggestion" | "warning" | "error" max_file_size = 500, -- skip files larger than N lines (0 = unlimited) claude_cli = { cmd = "claude", agent = "code-review" }, codex_cli = { cmd = "codex", model = nil }, gemini_cli = { cmd = "gemini", model = nil }, + qwen_cli = { cmd = "qwen", model = nil }, anthropic = { api_key = nil, model = "claude-sonnet-4-20250514" }, openai = { api_key = nil, model = "gpt-4o", base_url = nil }, ollama = { model = "llama3", base_url = "http://localhost:11434" }, @@ -189,6 +190,7 @@ Set `ai.provider` to choose a backend. Each provider has its own sub-table: | Claude CLI | `claude_cli` | [Claude CLI](https://docs.anthropic.com/en/docs/claude-code) installed | | Codex CLI | `codex_cli` | [Codex CLI](https://github.com/openai/codex) installed and authenticated | | Gemini CLI | `gemini_cli` | [Gemini CLI](https://github.com/google-gemini/gemini-cli) installed and authenticated | +| Qwen CLI | `qwen_cli` | [Qwen CLI](https://github.com/QwenLM/qwen-code) installed and authenticated | | Anthropic API | `anthropic` | `api_key` set | | OpenAI | `openai` | `api_key` set | | Ollama | `ollama` | Ollama running locally | diff --git a/doc/codereview.txt b/doc/codereview.txt index bfbbee0..770bde0 100644 --- a/doc/codereview.txt +++ b/doc/codereview.txt @@ -306,7 +306,7 @@ Options for AI review. See |codereview-ai-review|. -- boolean enable AI review features enabled = true, - -- "claude_cli" | "codex_cli" | "gemini_cli" | "anthropic" | "openai" | "ollama" | "custom_cmd" + -- "claude_cli" | "codex_cli" | "gemini_cli" | "qwen_cli" | "anthropic" | "openai" | "ollama" | "custom_cmd" provider = "claude_cli", -- "info" | "suggestion" | "warning" | "error" minimum severity to display @@ -329,6 +329,10 @@ Options for AI review. See |codereview-ai-review|. cmd = "gemini", -- path or name of the gemini binary model = nil, -- optional model override }, + qwen_cli = { + cmd = "qwen", -- path or name of the qwen binary + model = nil, -- optional model override + }, anthropic = { api_key = nil, -- falls back to ANTHROPIC_API_KEY model = "claude-sonnet-4-20250514", @@ -426,6 +430,25 @@ Requirements: } < +------------------------------------------------------------------------------ +Qwen CLI *codereview-ai-qwen-cli* + +Uses the `qwen` command-line interface to prompt non-interactively using +the currently configured or default model until explicitly specified. + +Requirements: + - `qwen` binary installed and on PATH + - Qwen CLI already authenticated +>lua + ai = { + provider = "qwen_cli", + qwen_cli = { + cmd = "qwen", -- binary name or full path + model = "qwen3-coder-plus", -- optional model override + }, + } +< + ------------------------------------------------------------------------------ Anthropic API *codereview-ai-anthropic* diff --git a/lua/codereview/ai/providers/init.lua b/lua/codereview/ai/providers/init.lua index 9bd244a..316e346 100644 --- a/lua/codereview/ai/providers/init.lua +++ b/lua/codereview/ai/providers/init.lua @@ -4,6 +4,7 @@ local valid_providers = { claude_cli = "codereview.ai.providers.claude_cli", codex_cli = "codereview.ai.providers.codex_cli", gemini_cli = "codereview.ai.providers.gemini_cli", + qwen_cli = "codereview.ai.providers.qwen_cli", anthropic = "codereview.ai.providers.anthropic", openai = "codereview.ai.providers.openai", ollama = "codereview.ai.providers.ollama", diff --git a/lua/codereview/ai/providers/qwen_cli.lua b/lua/codereview/ai/providers/qwen_cli.lua new file mode 100644 index 0000000..5b67aea --- /dev/null +++ b/lua/codereview/ai/providers/qwen_cli.lua @@ -0,0 +1,27 @@ +local config = require("codereview.config") +local utils = require("codereview.ai.providers.utils") +local M = {} + +function M.build_cmd(qwen_cmd, model) + local cmd = { qwen_cmd or "qwen" } + table.insert(cmd, "--approval-mode=plan") + if model then + table.insert(cmd, "--model") + table.insert(cmd, model) + end + return cmd +end + +function M.run(prompt, callback) + local cfg = config.get() + if not cfg.ai.enabled then + callback(nil, "AI review is disabled in config") + return + end + + local pcfg = cfg.ai.qwen_cli or {} + local cmd = M.build_cmd(pcfg.cmd, pcfg.model) + return utils.run_cli(prompt, callback, cmd) +end + +return M diff --git a/lua/codereview/config.lua b/lua/codereview/config.lua index 4662c9f..03d4e32 100644 --- a/lua/codereview/config.lua +++ b/lua/codereview/config.lua @@ -27,12 +27,13 @@ ---@class codereview.config.AI ---@field enabled? boolean enable AI Review (default: `true`) ----@field provider? "claude_cli"|"codex_cli"|"gemini_cli"|"anthropic"|"openai"|"ollama"|"custom_cmd" AI Provider to use +---@field provider? "claude_cli"|"codex_cli"|"gemini_cli"|"qwen_cli"|"anthropic"|"openai"|"ollama"|"custom_cmd" AI Provider to use ---@field review_level? "info"|"suggestion"|"warning"|"error" controls the verbosity of AI code reviews (default: `info`) ---@field max_file_size? integer skip files larger than N lines (0 = unlimited) (default: 500) ---@field claude_cli? codereview.config.ai.ClaudeCli Claude CLI options ---@field codex_cli? codereview.config.ai.CodexCLI Codex CLI options ---@field gemini_cli? codereview.config.ai.GeminiCLI Gemini CLI options +---@field qwen_cli? codereview.config.ai.QwenCLI Qwen CLI options ---@field anthropic? codereview.config.ai.Anthropic Anthropic API options ---@field openai? codereview.config.ai.OpenAI OpenAI API options ---@field ollama? codereview.config.ai.Ollama Ollama options @@ -50,6 +51,10 @@ ---@field cmd? string Gemini CLI command (default: `gemini`) ---@field model? string Gemini model name +---@class codereview.config.ai.QwenCLI +---@field cmd? string Qwen CLI command (default: `qwen`) +---@field model? string Qwen model name + ---@class codereview.config.ai.Anthropic ---@field api_key? string Anthropic API key ---@field model? string Anthropic model name @@ -89,6 +94,7 @@ local defaults = { claude_cli = { cmd = "claude", agent = "code-review" }, codex_cli = { cmd = "codex", model = nil }, gemini_cli = { cmd = "gemini", model = nil }, + qwen_cli = { cmd = "qwen", model = nil }, anthropic = { api_key = nil, model = "claude-sonnet-4-20250514" }, openai = { api_key = nil, model = "gpt-4o", base_url = nil }, ollama = { model = "llama3", base_url = "http://localhost:11434" }, @@ -128,6 +134,7 @@ local function validate(c) claude_cli = true, codex_cli = true, gemini_cli = true, + qwen_cli = true, anthropic = true, openai = true, ollama = true, diff --git a/tests/codereview/ai/providers/qwen_cli_spec.lua b/tests/codereview/ai/providers/qwen_cli_spec.lua new file mode 100644 index 0000000..af4ff45 --- /dev/null +++ b/tests/codereview/ai/providers/qwen_cli_spec.lua @@ -0,0 +1,21 @@ +package.loaded["codereview.config"] = { + get = function() + return { + ai = { enabled = true, provider = "qwen_cli", qwen_cli = { cmd = "qwen", model = nil } }, + } + end, +} +package.loaded["codereview.log"] = { debug = function() end, warn = function() end, error = function() end } + +local qwen_cli = require("codereview.ai.providers.qwen_cli") + +describe("ai.providers.qwen_cli", function() + describe("build_cmd", function() + it("builds default qwen command", function() + assert.same({ "qwen", "--approval-mode=plan" }, qwen_cli.build_cmd("qwen")) + end) + it("includes model flag when provided", function() + assert.same({ "qwen", "--approval-mode=plan", "--model", "model" }, qwen_cli.build_cmd("qwen", "model")) + end) + end) +end) From 6dd56067cbf0819ed16530b8d72337b1b3505331 Mon Sep 17 00:00:00 2001 From: Levente Polyak Date: Tue, 24 Mar 2026 21:07:16 +0100 Subject: [PATCH 5/8] feat(ai): Add GitHub Copilot CLI integration --- README.md | 4 ++- doc/codereview.txt | 27 +++++++++++++++- lua/codereview/ai/providers/copilot_cli.lua | 32 +++++++++++++++++++ lua/codereview/ai/providers/init.lua | 1 + lua/codereview/config.lua | 10 +++++- .../ai/providers/copilot_cli_spec.lua | 30 +++++++++++++++++ 6 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 lua/codereview/ai/providers/copilot_cli.lua create mode 100644 tests/codereview/ai/providers/copilot_cli_spec.lua diff --git a/README.md b/README.md index e133906..1829c65 100644 --- a/README.md +++ b/README.md @@ -159,12 +159,13 @@ require("codereview").setup({ -- AI review ai = { enabled = true, - provider = "claude_cli", -- "claude_cli" | "codex_cli" | "gemini_cli" | "qwen_cli" | "anthropic" | "openai" | "ollama" | "custom_cmd" + provider = "claude_cli", -- "claude_cli" | "codex_cli" | "copilot_cli" | "gemini_cli" | "qwen_cli" | "anthropic" | "openai" | "ollama" | "custom_cmd" review_level = "info", -- "info" | "suggestion" | "warning" | "error" max_file_size = 500, -- skip files larger than N lines (0 = unlimited) claude_cli = { cmd = "claude", agent = "code-review" }, codex_cli = { cmd = "codex", model = nil }, + copilot_cli = { cmd = "copilot", model = nil, agent = nil }, gemini_cli = { cmd = "gemini", model = nil }, qwen_cli = { cmd = "qwen", model = nil }, anthropic = { api_key = nil, model = "claude-sonnet-4-20250514" }, @@ -189,6 +190,7 @@ Set `ai.provider` to choose a backend. Each provider has its own sub-table: |----------|-----------|--------------| | Claude CLI | `claude_cli` | [Claude CLI](https://docs.anthropic.com/en/docs/claude-code) installed | | Codex CLI | `codex_cli` | [Codex CLI](https://github.com/openai/codex) installed and authenticated | +| Copilot CLI | `copilot_cli` | [Copilot CLI](https://docs.github.com/en/copilot/concepts/agents/copilot-cli/about-copilot-cli) installed and authenticated | | Gemini CLI | `gemini_cli` | [Gemini CLI](https://github.com/google-gemini/gemini-cli) installed and authenticated | | Qwen CLI | `qwen_cli` | [Qwen CLI](https://github.com/QwenLM/qwen-code) installed and authenticated | | Anthropic API | `anthropic` | `api_key` set | diff --git a/doc/codereview.txt b/doc/codereview.txt index 770bde0..20cbedc 100644 --- a/doc/codereview.txt +++ b/doc/codereview.txt @@ -306,7 +306,7 @@ Options for AI review. See |codereview-ai-review|. -- boolean enable AI review features enabled = true, - -- "claude_cli" | "codex_cli" | "gemini_cli" | "qwen_cli" | "anthropic" | "openai" | "ollama" | "custom_cmd" + -- "claude_cli" | "codex_cli" | "copilot_cli" | "gemini_cli" | "qwen_cli" | "anthropic" | "openai" | "ollama" | "custom_cmd" provider = "claude_cli", -- "info" | "suggestion" | "warning" | "error" minimum severity to display @@ -325,6 +325,11 @@ Options for AI review. See |codereview-ai-review|. cmd = "codex", -- path or name of the codex binary model = nil, -- optional model override }, + copilot_cli = { + cmd = "copilot", -- path or name of the copilot binary + model = nil, -- optional model override + agent = nil, -- optional agent override + }, gemini_cli = { cmd = "gemini", -- path or name of the gemini binary model = nil, -- optional model override @@ -411,6 +416,26 @@ Requirements: } < +------------------------------------------------------------------------------ +Copilot CLI *codereview-ai-copilot-cli* + +Uses the `copilot` command-line interface to prompt non-interactively using +the currently configured or default model until explicitly specified. + +Requirements: + - `copilot` binary installed and on PATH + - Copilot CLI already authenticated +>lua + ai = { + provider = "copilot_cli", + copilot_cli = { + cmd = "copilot", -- binary name or full path + model = "claude-opus-4.6", -- optional model override + agent = nil -- specify a custom agent to use + }, + } +< + ------------------------------------------------------------------------------ Gemini CLI *codereview-ai-gemini-cli* diff --git a/lua/codereview/ai/providers/copilot_cli.lua b/lua/codereview/ai/providers/copilot_cli.lua new file mode 100644 index 0000000..5910df1 --- /dev/null +++ b/lua/codereview/ai/providers/copilot_cli.lua @@ -0,0 +1,32 @@ +local config = require("codereview.config") +local utils = require("codereview.ai.providers.utils") +local M = {} + +function M.build_cmd(copilot_cmd, model, agent) + local cmd = { copilot_cmd or "copilot" } + table.insert(cmd, "--silent") + table.insert(cmd, "--no-auto-update") + if model and model ~= "" then + table.insert(cmd, "--model") + table.insert(cmd, model) + end + if agent and agent ~= "" then + table.insert(cmd, "--agent") + table.insert(cmd, agent) + end + return cmd +end + +function M.run(prompt, callback) + local cfg = config.get() + if not cfg.ai.enabled then + callback(nil, "AI review is disabled in config") + return + end + + local pcfg = cfg.ai.copilot_cli or {} + local cmd = M.build_cmd(pcfg.cmd, pcfg.model, pcfg.agent) + return utils.run_cli(prompt, callback, cmd) +end + +return M diff --git a/lua/codereview/ai/providers/init.lua b/lua/codereview/ai/providers/init.lua index 316e346..aa9dbc9 100644 --- a/lua/codereview/ai/providers/init.lua +++ b/lua/codereview/ai/providers/init.lua @@ -3,6 +3,7 @@ local M = {} local valid_providers = { claude_cli = "codereview.ai.providers.claude_cli", codex_cli = "codereview.ai.providers.codex_cli", + copilot_cli = "codereview.ai.providers.copilot_cli", gemini_cli = "codereview.ai.providers.gemini_cli", qwen_cli = "codereview.ai.providers.qwen_cli", anthropic = "codereview.ai.providers.anthropic", diff --git a/lua/codereview/config.lua b/lua/codereview/config.lua index 03d4e32..2c9869c 100644 --- a/lua/codereview/config.lua +++ b/lua/codereview/config.lua @@ -27,11 +27,12 @@ ---@class codereview.config.AI ---@field enabled? boolean enable AI Review (default: `true`) ----@field provider? "claude_cli"|"codex_cli"|"gemini_cli"|"qwen_cli"|"anthropic"|"openai"|"ollama"|"custom_cmd" AI Provider to use +---@field provider? "claude_cli"|"codex_cli"|"copilot_cli"|"gemini_cli"|"qwen_cli"|"anthropic"|"openai"|"ollama"|"custom_cmd" AI Provider to use ---@field review_level? "info"|"suggestion"|"warning"|"error" controls the verbosity of AI code reviews (default: `info`) ---@field max_file_size? integer skip files larger than N lines (0 = unlimited) (default: 500) ---@field claude_cli? codereview.config.ai.ClaudeCli Claude CLI options ---@field codex_cli? codereview.config.ai.CodexCLI Codex CLI options +---@field copilot_cli? codereview.config.ai.CopilotCLI Copilot CLI options ---@field gemini_cli? codereview.config.ai.GeminiCLI Gemini CLI options ---@field qwen_cli? codereview.config.ai.QwenCLI Qwen CLI options ---@field anthropic? codereview.config.ai.Anthropic Anthropic API options @@ -47,6 +48,11 @@ ---@field cmd? string Codex CLI command (default: `codex`) ---@field model? string Codex model name +---@class codereview.config.ai.CopilotCLI +---@field cmd? string Copilot CLI command (default: `copilot`) +---@field model? string Copilot model name +---@field agent? string specify a custom agent to use + ---@class codereview.config.ai.GeminiCLI ---@field cmd? string Gemini CLI command (default: `gemini`) ---@field model? string Gemini model name @@ -93,6 +99,7 @@ local defaults = { max_file_size = 500, claude_cli = { cmd = "claude", agent = "code-review" }, codex_cli = { cmd = "codex", model = nil }, + copilot_cli = { cmd = "copilot", model = nil, agent = nil }, gemini_cli = { cmd = "gemini", model = nil }, qwen_cli = { cmd = "qwen", model = nil }, anthropic = { api_key = nil, model = "claude-sonnet-4-20250514" }, @@ -133,6 +140,7 @@ local function validate(c) local valid_providers = { claude_cli = true, codex_cli = true, + copilot_cli = true, gemini_cli = true, qwen_cli = true, anthropic = true, diff --git a/tests/codereview/ai/providers/copilot_cli_spec.lua b/tests/codereview/ai/providers/copilot_cli_spec.lua new file mode 100644 index 0000000..40cd262 --- /dev/null +++ b/tests/codereview/ai/providers/copilot_cli_spec.lua @@ -0,0 +1,30 @@ +package.loaded["codereview.config"] = { + get = function() + return { + ai = { enabled = true, provider = "copilot_cli", copilot_cli = { cmd = "copilot", model = nil, agent = nil } }, + } + end, +} +package.loaded["codereview.log"] = { debug = function() end, warn = function() end, error = function() end } + +local copilot_cli = require("codereview.ai.providers.copilot_cli") + +describe("ai.providers.copilot_cli", function() + describe("build_cmd", function() + it("builds default copilot command", function() + assert.same({ "copilot", "--silent", "--no-auto-update" }, copilot_cli.build_cmd("copilot")) + end) + it("includes model flag when provided", function() + assert.same( + { "copilot", "--silent", "--no-auto-update", "--model", "model" }, + copilot_cli.build_cmd("copilot", "model") + ) + end) + it("includes agent flag when provided", function() + assert.same( + { "copilot", "--silent", "--no-auto-update", "--agent", "agent" }, + copilot_cli.build_cmd("copilot", nil, "agent") + ) + end) + end) +end) From 60924a51c2f1fa78c0b185e1a13c8043b79352d9 Mon Sep 17 00:00:00 2001 From: Levente Polyak Date: Tue, 24 Mar 2026 21:18:25 +0100 Subject: [PATCH 6/8] feat(ai): Add OpenCode CLI integration --- README.md | 4 ++- doc/codereview.txt | 29 ++++++++++++++- lua/codereview/ai/providers/init.lua | 1 + lua/codereview/ai/providers/opencode_cli.lua | 35 +++++++++++++++++++ lua/codereview/config.lua | 11 +++++- .../ai/providers/opencode_cli_spec.lua | 34 ++++++++++++++++++ 6 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 lua/codereview/ai/providers/opencode_cli.lua create mode 100644 tests/codereview/ai/providers/opencode_cli_spec.lua diff --git a/README.md b/README.md index 1829c65..87d85ac 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ require("codereview").setup({ -- AI review ai = { enabled = true, - provider = "claude_cli", -- "claude_cli" | "codex_cli" | "copilot_cli" | "gemini_cli" | "qwen_cli" | "anthropic" | "openai" | "ollama" | "custom_cmd" + provider = "claude_cli", -- "claude_cli" | "codex_cli" | "copilot_cli" | "gemini_cli" | "opencode_cli" | "qwen_cli" | "anthropic" | "openai" | "ollama" | "custom_cmd" review_level = "info", -- "info" | "suggestion" | "warning" | "error" max_file_size = 500, -- skip files larger than N lines (0 = unlimited) @@ -167,6 +167,7 @@ require("codereview").setup({ codex_cli = { cmd = "codex", model = nil }, copilot_cli = { cmd = "copilot", model = nil, agent = nil }, gemini_cli = { cmd = "gemini", model = nil }, + opencode_cli = { cmd = "opencode", model = nil, agent = "plan", variant = nil }, qwen_cli = { cmd = "qwen", model = nil }, anthropic = { api_key = nil, model = "claude-sonnet-4-20250514" }, openai = { api_key = nil, model = "gpt-4o", base_url = nil }, @@ -192,6 +193,7 @@ Set `ai.provider` to choose a backend. Each provider has its own sub-table: | Codex CLI | `codex_cli` | [Codex CLI](https://github.com/openai/codex) installed and authenticated | | Copilot CLI | `copilot_cli` | [Copilot CLI](https://docs.github.com/en/copilot/concepts/agents/copilot-cli/about-copilot-cli) installed and authenticated | | Gemini CLI | `gemini_cli` | [Gemini CLI](https://github.com/google-gemini/gemini-cli) installed and authenticated | +| OpenCode CLI | `opencode_cli` | [OpenCode CLI](https://github.com/anomalyco/opencode) installed and authenticated | | Qwen CLI | `qwen_cli` | [Qwen CLI](https://github.com/QwenLM/qwen-code) installed and authenticated | | Anthropic API | `anthropic` | `api_key` set | | OpenAI | `openai` | `api_key` set | diff --git a/doc/codereview.txt b/doc/codereview.txt index 20cbedc..679ea36 100644 --- a/doc/codereview.txt +++ b/doc/codereview.txt @@ -306,7 +306,7 @@ Options for AI review. See |codereview-ai-review|. -- boolean enable AI review features enabled = true, - -- "claude_cli" | "codex_cli" | "copilot_cli" | "gemini_cli" | "qwen_cli" | "anthropic" | "openai" | "ollama" | "custom_cmd" + -- "claude_cli" | "codex_cli" | "copilot_cli" | "gemini_cli" | "opencode_cli" | "qwen_cli" | "anthropic" | "openai" | "ollama" | "custom_cmd" provider = "claude_cli", -- "info" | "suggestion" | "warning" | "error" minimum severity to display @@ -334,6 +334,12 @@ Options for AI review. See |codereview-ai-review|. cmd = "gemini", -- path or name of the gemini binary model = nil, -- optional model override }, + opencode_cli = { + cmd = "opencode", -- path or name of the opencode binary + model = nil, -- optional model override + agent = "plan", -- optional agent override + variant = nil, -- optional model variant (provider-specific) + }, qwen_cli = { cmd = "qwen", -- path or name of the qwen binary model = nil, -- optional model override @@ -455,6 +461,27 @@ Requirements: } < +------------------------------------------------------------------------------ +OpenCode CLI *codereview-ai-opencode-cli* + +Uses the `opencode run` command-line interface to prompt non-interactively using +the currently configured or default model until explicitly specified. + +Requirements: + - `opencode` binary installed and on PATH + - OpenCode CLI already authenticated +>lua + ai = { + provider = "opencode_cli", + opencode_cli = { + cmd = "opencode", -- binary name or full path + model = "github-copilot/claude-sonnet-4.6", -- optional provider/model + agent = "plan", -- optional agent to use + variant = nil, -- model variant + }, + } +< + ------------------------------------------------------------------------------ Qwen CLI *codereview-ai-qwen-cli* diff --git a/lua/codereview/ai/providers/init.lua b/lua/codereview/ai/providers/init.lua index aa9dbc9..5d25930 100644 --- a/lua/codereview/ai/providers/init.lua +++ b/lua/codereview/ai/providers/init.lua @@ -5,6 +5,7 @@ local valid_providers = { codex_cli = "codereview.ai.providers.codex_cli", copilot_cli = "codereview.ai.providers.copilot_cli", gemini_cli = "codereview.ai.providers.gemini_cli", + opencode_cli = "codereview.ai.providers.opencode_cli", qwen_cli = "codereview.ai.providers.qwen_cli", anthropic = "codereview.ai.providers.anthropic", openai = "codereview.ai.providers.openai", diff --git a/lua/codereview/ai/providers/opencode_cli.lua b/lua/codereview/ai/providers/opencode_cli.lua new file mode 100644 index 0000000..f322d7b --- /dev/null +++ b/lua/codereview/ai/providers/opencode_cli.lua @@ -0,0 +1,35 @@ +local config = require("codereview.config") +local utils = require("codereview.ai.providers.utils") +local M = {} + +function M.build_cmd(opencode_cmd, model, agent, variant) + local cmd = { opencode_cmd or "opencode", "run" } + if model and model ~= "" then + table.insert(cmd, "--model") + table.insert(cmd, model) + end + if agent and agent ~= "" then + table.insert(cmd, "--agent") + table.insert(cmd, agent) + end + if variant and variant ~= "" then + table.insert(cmd, "--variant") + table.insert(cmd, variant) + end + table.insert(cmd, "--") + return cmd +end + +function M.run(prompt, callback) + local cfg = config.get() + if not cfg.ai.enabled then + callback(nil, "AI review is disabled in config") + return + end + + local pcfg = cfg.ai.opencode_cli or {} + local cmd = M.build_cmd(pcfg.cmd, pcfg.model, pcfg.agent, pcfg.variant) + return utils.run_cli(prompt, callback, cmd) +end + +return M diff --git a/lua/codereview/config.lua b/lua/codereview/config.lua index 2c9869c..9922124 100644 --- a/lua/codereview/config.lua +++ b/lua/codereview/config.lua @@ -27,13 +27,14 @@ ---@class codereview.config.AI ---@field enabled? boolean enable AI Review (default: `true`) ----@field provider? "claude_cli"|"codex_cli"|"copilot_cli"|"gemini_cli"|"qwen_cli"|"anthropic"|"openai"|"ollama"|"custom_cmd" AI Provider to use +---@field provider? "claude_cli"|"codex_cli"|"copilot_cli"|"gemini_cli"|"opencode_cli"|"qwen_cli"|"anthropic"|"openai"|"ollama"|"custom_cmd" AI Provider to use ---@field review_level? "info"|"suggestion"|"warning"|"error" controls the verbosity of AI code reviews (default: `info`) ---@field max_file_size? integer skip files larger than N lines (0 = unlimited) (default: 500) ---@field claude_cli? codereview.config.ai.ClaudeCli Claude CLI options ---@field codex_cli? codereview.config.ai.CodexCLI Codex CLI options ---@field copilot_cli? codereview.config.ai.CopilotCLI Copilot CLI options ---@field gemini_cli? codereview.config.ai.GeminiCLI Gemini CLI options +---@field opencode_cli? codereview.config.ai.OpenCodeCLI OpenCode CLI options ---@field qwen_cli? codereview.config.ai.QwenCLI Qwen CLI options ---@field anthropic? codereview.config.ai.Anthropic Anthropic API options ---@field openai? codereview.config.ai.OpenAI OpenAI API options @@ -57,6 +58,12 @@ ---@field cmd? string Gemini CLI command (default: `gemini`) ---@field model? string Gemini model name +---@class codereview.config.ai.OpenCodeCLI +---@field cmd? string OpenCode CLI command (default: `opencode`) +---@field model? string model to use in the form of provider/model +---@field agent? string agent to use (default: `plan`) +---@field variant? string model variant (provider-specific reasoning effort, e.g., high, max, minimal) + ---@class codereview.config.ai.QwenCLI ---@field cmd? string Qwen CLI command (default: `qwen`) ---@field model? string Qwen model name @@ -101,6 +108,7 @@ local defaults = { codex_cli = { cmd = "codex", model = nil }, copilot_cli = { cmd = "copilot", model = nil, agent = nil }, gemini_cli = { cmd = "gemini", model = nil }, + opencode_cli = { cmd = "opencode", model = nil, agent = "plan", variant = nil }, qwen_cli = { cmd = "qwen", model = nil }, anthropic = { api_key = nil, model = "claude-sonnet-4-20250514" }, openai = { api_key = nil, model = "gpt-4o", base_url = nil }, @@ -142,6 +150,7 @@ local function validate(c) codex_cli = true, copilot_cli = true, gemini_cli = true, + opencode_cli = true, qwen_cli = true, anthropic = true, openai = true, diff --git a/tests/codereview/ai/providers/opencode_cli_spec.lua b/tests/codereview/ai/providers/opencode_cli_spec.lua new file mode 100644 index 0000000..b2a9f42 --- /dev/null +++ b/tests/codereview/ai/providers/opencode_cli_spec.lua @@ -0,0 +1,34 @@ +package.loaded["codereview.config"] = { + get = function() + return { + ai = { + enabled = true, + provider = "opencode_cli", + opencode_cli = { cmd = "opencode", model = nil, agent = "plan", variant = nil }, + }, + } + end, +} +package.loaded["codereview.log"] = { debug = function() end, warn = function() end, error = function() end } + +local opencode_cli = require("codereview.ai.providers.opencode_cli") + +describe("ai.providers.opencode_cli", function() + describe("build_cmd", function() + it("builds default opencode command", function() + assert.same({ "opencode", "run", "--" }, opencode_cli.build_cmd("opencode")) + end) + it("includes model flag when provided", function() + assert.same({ "opencode", "run", "--model", "model", "--" }, opencode_cli.build_cmd("opencode", "model")) + end) + it("includes agent flag when provided", function() + assert.same({ "opencode", "run", "--agent", "agent", "--" }, opencode_cli.build_cmd("opencode", nil, "agent")) + end) + it("includes variant flag when provided", function() + assert.same( + { "opencode", "run", "--variant", "variant", "--" }, + opencode_cli.build_cmd("opencode", nil, nil, "variant") + ) + end) + end) +end) From 8a6c9da382fd5e564f76cd376ba9926383c822c0 Mon Sep 17 00:00:00 2001 From: Levente Polyak Date: Tue, 24 Mar 2026 21:23:13 +0100 Subject: [PATCH 7/8] fix(config): Fix default value type declaration for claude_cli cmd --- lua/codereview/config.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/codereview/config.lua b/lua/codereview/config.lua index 9922124..1659964 100644 --- a/lua/codereview/config.lua +++ b/lua/codereview/config.lua @@ -42,7 +42,7 @@ ---@field custom_cmd? codereview.config.ai.CustomCmd Custom command options ---@class codereview.config.ai.ClaudeCli ----@field cmd? string Claude CLI command (default: `claude_cli`) +---@field cmd? string Claude CLI command (default: `claude`) ---@field agent? string Claude Agent (default: `code-review`) ---@class codereview.config.ai.CodexCLI From cef771dd66c64fda8913faf179dbff4242af6368 Mon Sep 17 00:00:00 2001 From: Levente Polyak Date: Thu, 26 Mar 2026 21:48:27 +0100 Subject: [PATCH 8/8] chore(config): rename ClaudeCLI class type in line with docs We use ClaudeCLI everywhere in the readme and docs. Also the general accepted spelling is all uppercase for CLI as it consists of 3 words, including how Claude writes about its CLI Reference. --- lua/codereview/config.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/codereview/config.lua b/lua/codereview/config.lua index 1659964..3b7b0ff 100644 --- a/lua/codereview/config.lua +++ b/lua/codereview/config.lua @@ -30,7 +30,7 @@ ---@field provider? "claude_cli"|"codex_cli"|"copilot_cli"|"gemini_cli"|"opencode_cli"|"qwen_cli"|"anthropic"|"openai"|"ollama"|"custom_cmd" AI Provider to use ---@field review_level? "info"|"suggestion"|"warning"|"error" controls the verbosity of AI code reviews (default: `info`) ---@field max_file_size? integer skip files larger than N lines (0 = unlimited) (default: 500) ----@field claude_cli? codereview.config.ai.ClaudeCli Claude CLI options +---@field claude_cli? codereview.config.ai.ClaudeCLI Claude CLI options ---@field codex_cli? codereview.config.ai.CodexCLI Codex CLI options ---@field copilot_cli? codereview.config.ai.CopilotCLI Copilot CLI options ---@field gemini_cli? codereview.config.ai.GeminiCLI Gemini CLI options @@ -41,7 +41,7 @@ ---@field ollama? codereview.config.ai.Ollama Ollama options ---@field custom_cmd? codereview.config.ai.CustomCmd Custom command options ----@class codereview.config.ai.ClaudeCli +---@class codereview.config.ai.ClaudeCLI ---@field cmd? string Claude CLI command (default: `claude`) ---@field agent? string Claude Agent (default: `code-review`)