Skip to content

Conversation

@Vedaant-Rajoo
Copy link
Owner

@Vedaant-Rajoo Vedaant-Rajoo commented Jan 5, 2026

Summary by CodeRabbit

  • New Features

    • Initial release of clai: an AI-powered shell command translator
    • Generate shell commands from natural language instructions via AI providers
    • Interactive command selection and execution with safety confirmations
    • Context-aware command generation using system info, history, and current environment
    • Dangerous command detection with customizable patterns and user prompts
    • Multi-provider support with automatic fallback capabilities
    • Comprehensive configuration system supporting files, environment variables, and CLI flags
    • Debug logging and diagnostics for troubleshooting
  • Documentation

    • Added project README and contribution guidelines
  • Tests & Build

    • Benchmarking suite and integration tests for core functionality

✏️ Tip: You can customize this high-level summary in your review settings.

Greptile Summary

This PR implements a complete Rust-based CLI tool that converts natural language instructions into shell commands using AI providers (OpenRouter). The implementation is well-architected with strong separation of concerns across modules.

Key Features Implemented:

  • AI-powered command generation with multi-provider support and fallback capabilities
  • Comprehensive safety system with dangerous command detection using regex patterns
  • Interactive mode with Tab-cycling through multiple command suggestions
  • Shell history and system context integration for improved AI suggestions
  • Robust error handling with specific exit codes (1-5) for different error types
  • Config file management with strict security (0600 permissions on Unix)
  • File logging for debugging and audit trails

Architecture Highlights:

  • Clean modular structure: AI, safety, config, context, error handling modules
  • Pure functional approach where possible with isolated I/O side effects
  • Proper async/await with Tokio for API calls
  • Signal handling (SIGINT, SIGTERM, SIGPIPE) following UNIX conventions
  • Stdout/stderr separation: commands to stdout, logs/diagnostics to stderr

Code Quality:

  • Well-documented with comprehensive inline comments and examples
  • Extensive unit tests across all modules (100+ test cases)
  • No unsafe blocks detected in the codebase
  • Proper error propagation using Result types and custom error enums
  • Release profile optimized with LTO and stripping

Minor Considerations:

  • Integration tests are basic and assume pre-built binary - could benefit from more comprehensive end-to-end testing
  • Some shell scripts for manual testing could be converted to automated integration tests
  • The test_valid_instruction_parses test in tests/cli_tests.rs:33-38 doesn't actually validate AI integration works

Confidence Score: 4/5

  • This PR is safe to merge with minor considerations around test coverage
  • Strong implementation with excellent safety features, error handling, and code structure. Score of 4 (not 5) due to limited integration test coverage - the existing CLI tests don't fully validate AI provider integration and command generation accuracy. All critical security aspects (dangerous command detection, config file permissions, error handling) are properly implemented.
  • tests/cli_tests.rs could benefit from more comprehensive integration tests that validate actual AI command generation

Important Files Changed

Filename Overview
src/main.rs Main entry point with comprehensive error handling, signal management, and structured control flow. Well-architected with proper separation of concerns.
src/safety/detector.rs Dangerous command detection using regex patterns with fail-safe defaults. Properly tested and handles edge cases well.
src/safety/patterns.rs Regex pattern compilation with caching and comprehensive default dangerous patterns. Good error handling for invalid patterns.
src/ai/providers/openrouter.rs OpenRouter API integration with retry logic, proper error handling, and comprehensive logging. Authentication and rate limiting handled correctly.
src/config/loader.rs Config loading with Unix permission checks (0600 requirement). Proper error handling and environment variable resolution.
src/error/mod.rs Well-structured error types with specific exit codes, proper error chain handling, and file logging integration. Clean conversion implementations.
src/ai/prompt.rs Prompt building and command extraction with comprehensive fallbacks. Handles multiple response formats gracefully. Well-tested.
tests/cli_tests.rs Basic CLI integration tests but limited coverage. Tests assume binary is already built and don't validate actual command generation.

Vedaant-Rajoo and others added 6 commits January 3, 2026 13:52
…installation instructions, configuration options, usage examples, and development guidelines.
Implements a new -d/--debug CLI option that displays the complete
request that will be sent to the AI provider, including:
- Model, temperature, and max tokens settings
- System and user messages
- Full context (system info, directory, history, user instruction)

Changes:
- Added 'debug' field to CLI struct (src/cli/mod.rs)
- Added 'debug' field to Config struct (src/config/mod.rs)
- Propagated debug flag through all Config constructions
- Added debug output in generate_command() and generate_commands()
- Updated all test cases to include debug field
- Debug output goes to stderr via eprintln!, preserving stdout for piping

The debug flag helps developers understand what context is being sent
to the AI, useful for debugging unexpected AI responses or verifying
context gathering.
…ion-8abRI

Add --debug flag to display AI prompts before sending
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Fix all issues with AI Agents 🤖
In @.cargo/config.toml:
- Around line 28-29: The Cargo aliases `lint` and `pre-commit` in
.cargo/config.toml use shell operators (`&&`) which Cargo does not support;
remove or replace these aliases so they don't contain `&&` and instead either
(a) document and create separate aliases that run single cargo subcommands
(e.g., `fmt` and `clippy`) or (b) implement the combined workflow as a shell
script in a scripts/ directory or via a cargo-make/Makefile.toml task, and
update documentation to instruct contributors to run the script/task for
combined checks.
- Around line 1-3: The README/example comment references the wrong alias name:
it shows "cargo rb" but the defined alias is "br"; update the documentation
example to use "cargo br" (or rename the alias to "rb" if you intended that
name) so the doc and the alias in .cargo/config.toml (alias "br") match; ensure
the example lines that currently say "cargo rb" are changed to "cargo br" for
consistency.

In @.cursor/commands/tm/auto-implement-tasks.md:
- Around line 59-68: Add a language identifier to the fenced code block that
begins after the "Step-by-step with validation:" line by changing the opening
triple backticks to include a language (e.g., ```text) so the block conforms to
MD040; update the opening fence containing "```" to "```text" (or another
appropriate language) and leave the closing "```" unchanged.

In @.cursor/commands/tm/help.md:
- Around line 84-89: The fenced code block containing the Taskmaster commands
(e.g., "/taskmaster:list-tasks", "/taskmaster:show-task 1.2",
"/taskmaster:add-task", "/taskmaster:next-task") is missing a language specifier
and violates MD040; update the opening fence to include a language such as bash
(change the opening "```" to "```bash") so the block becomes a valid fenced code
block with a language identifier.

In @.cursor/commands/tm/install-taskmaster.md:
- Around line 103-110: The fenced code block starting at the displayed snippet
lacks a language specifier; update the opening fence in the snippet (the block
containing the Task Master installation output) to use a language identifier by
changing the opening "```" to "```text" so the block conforms to MD040 and
renders as plain output.

In @.cursor/commands/tm/remove-dependency.md:
- Around line 55-62: The fenced code block showing the example command
"/taskmaster:remove-dependency 5 from 3" lacks a language identifier; update the
opening fence from "```" to "```bash" (or another appropriate language) so the
block becomes "```bash" followed by the same example lines and the closing
"```", satisfying MD040 and enabling proper syntax highlighting.

In @.cursor/commands/tm/remove-task.md:
- Around line 39-51: The fenced code block showing "Task Removal Impact" lacks a
language identifier; update the opening fence (the triple backticks that start
the block containing "Task Removal Impact" / "Task: #5 ...") to include a
language tag (e.g., change ``` to ```text) so the block becomes a
language-specified fenced code block for proper markdown linting and syntax
highlighting.
- Around line 87-99: The fenced code block(s) containing the example CLI output
(the block that starts with ``` and shows "/taskmaster:remove-task 5" and the
subsequent output) lack a language identifier; update those fence openers to
include a language tag (e.g., change ``` to ```text) so markdown linters and
syntax highlighters recognize them consistently.

In @.cursor/commands/tm/validate-dependencies.md:
- Around line 45-56: Update the fenced code block that begins with triple
backticks showing the "Dependency Validation Report" so it declares the language
as plain text by changing the opening fence from ``` to ```text; ensure you only
modify the opening fence (the closing fence stays as ```), leaving the report
contents unchanged.

In @.cursor/rules/taskmaster/dev_workflow.mdc:
- Line 200: Remove or replace the broken documentation references to
@taskmaster.mdc, @mcp.mdc, and assets/env.example in the dev_workflow doc:
either create those files with the missing CLI command list, MCP architecture
details, and env template, or inline the necessary content where the references
currently appear (replace the @taskmaster.mdc command list and @mcp.mdc
architecture summary with the actual text and add the environment variable
example in place of assets/env.example); ensure the updated text includes the
MCP tool command examples, a brief MCP architecture overview, and a complete env
example so no external references remain broken.
- Line 35: Replace all occurrences of the legacy `@taskmaster.mdc` reference
with the canonical link
`[taskmaster.mdc](mdc:.cursor/rules/taskmaster/taskmaster.mdc)` (update every
mention listed in the review); remove or replace the non-existent `@mcp.mdc`
reference by either adding the missing `mcp.mdc` file or inlining its content /
pointing that reference to
`[taskmaster.mdc](mdc:.cursor/rules/taskmaster/taskmaster.mdc)` as appropriate;
fix the broken `assets/env.example` reference by either adding the
`assets/env.example` file to the repo or changing the link to an existing file
(or include the required env example inline) so no unresolved file links remain.

In @.taskmaster/docs/prd.txt:
- Around line 1-10: Replace the placeholder title "<Unnamed CLI Tool>" on the
PRD header line with the actual product name "clAI" so the top banner reads
"clAI PRD v2" (update the string in the header block that currently contains
"<Unnamed CLI Tool>" to "clAI").

In @.taskmaster/tasks/tasks.json:
- Around line 288-293: Subtask "Implement OpenRouter Provider" (Subtask 4.2) is
marked "in-progress" but its "details" field contains completion notes; change
the task's "status" value from "in-progress" to "done" for the entry that
contains the OpenRouter implementation notes, and update the "updatedAt"
timestamp to reflect completion; ensure "testStrategy" remains accurate (or mark
tests as passed) so the metadata (status, details, updatedAt, testStrategy) is
consistent.
- Around line 21-24: The file uses the literal string "undefined" for parentId
in subtasks, which is invalid JSON and a JS artifact; update every occurrence of
the parentId property (e.g., the "parentId" entries shown in
.taskmaster/tasks/tasks.json) to either null or remove the parentId field
entirely for tasks that have no parent, ensuring the JSON remains valid and
consistent across all subtasks.

In @benches/startup.rs:
- Around line 38-59: The benchmark named parse_args currently constructs the Cli
struct directly (inside group.bench_function and using Cli { ... }) which
doesn't measure actual clap parsing; replace the manual construction with a call
to the clap parsing helpers (e.g., Cli::parse_from or Cli::try_parse_from)
feeding a simulated argv Vec or slice so the benchmark exercises clap's parsing
logic and keeps the same group.bench_function("parse_args", |b| { ... }) harness
and instruction value.
- Around line 138-143: The benchmark repeatedly calls setup_signal_handlers
inside group.bench_function("setup_signal_handlers"), which doesn't reflect
production (it's called once at startup); update the benchmark to either (A)
measure a single invocation by replacing the iter loop with a single call (so
the bench records one-time setup) or (B) remove the "setup_signal_handlers"
benchmark entirely if you don't want to measure repeated registration; locate
the group.bench_function invocation and change it accordingly to call
setup_signal_handlers only once or delete that bench entry.

In @Cargo.toml:
- Line 2: The Cargo package name "clAI" in Cargo.toml violates Rust naming
conventions; change the name value to all-lowercase (e.g., "clai") in the name =
"..." entry and update any workspace members, package references, CI configs, or
documentation that refer to "clAI" to use the new lowercase name so dependency
resolution and tooling remain consistent.

In @CONTRIBUTING.md:
- Line 11: Replace the placeholder repository URL
"https://github.com/yourusername/clAI.git" in CONTRIBUTING.md with the actual
repository path (the project's canonical GitHub URL), matching the same fix
applied in README.md so cloning instructions point to the real repo.

In @README.md:
- Line 15: Replace the placeholder repository URL in the README git clone line
by updating the string "git clone https://github.com/yourusername/clAI.git" to
use the actual GitHub owner or organization name (i.e., replace "yourusername"
with the real owner/org) so the clone command points to the real repository;
ensure the updated line accurately reflects the repository URL used for this
project.

In @src/ai/chain.rs:
- Around line 203-212: The is_available method currently only recognizes
"openrouter" (via OpenRouterProvider::api_key_from_env()) and returns false for
any other provider name, causing chains with unknown or future providers to be
marked unavailable; update the logic in is_available (the providers.iter().any
closure) so that the match returns true for unrecognized provider names (i.e.,
make the _ arm yield true) or alternatively swap to a provider registry lookup
and treat unknown providers as available by default; ensure you reference the
existing OpenRouterProvider::api_key_from_env() check and only keep
provider-specific checks where necessary.

In @src/ai/providers/openrouter.rs:
- Around line 119-245: The function make_request contains seven
developer-specific hardcoded debug blocks (marked with "// #region agent log")
that write to "/home/vee/Coding/clAI/.cursor/debug.log"; remove all these blocks
from make_request (including the OpenOptions/Write imports and the writeln!
calls) so the function no longer writes to that path, and if logging is required
replace them with calls to a proper logger (e.g., tracing or log) using the
response, status, and error data from the existing variables (response, status,
error_text, e, api_response) to preserve useful diagnostics; target the
make_request implementation in openrouter.rs and remove every "// #region agent
log" / matching block around request send, request error, response status, api
error, parse success, and parse error.

In @src/config/cache.rs:
- Around line 61-92: The test shows get_file_config currently reuses a global
cached config regardless of CLI arguments; fix by making the cache key include
relevant CLI fields (e.g., model, provider, instruction, context, offline flags
or a hash of the Cli struct) so get_file_config looks up/sets the cache per-CLI
rather than globally, update reset_config_cache if needed to clear per-key
entries, and add the suggested test (e.g., test_different_cli_args) that
constructs two different Cli values and asserts their resulting configs differ
to prevent regressions.

In @src/config/file.rs:
- Around line 114-123: There are two conflicting defaults: the function
default_dangerous_patterns() in src/config/file.rs returns simple substrings
while src/safety/patterns.rs uses stricter regexes (and includes Windows
patterns), causing mismatch when config patterns are compiled as regex; fix by
reconciling them — either convert default_dangerous_patterns() to return the
same regex strings as defined in src/safety/patterns.rs (including Windows
patterns like del/format C:) so defaults are valid regexes, or change the config
parsing/validation to treat config entries as plain substrings (and update docs
to state "config patterns are substrings"); additionally, centralize the
canonical default list (e.g., export from src/safety/patterns.rs or have config
call a single shared function) so both modules use the identical source of
truth.

In @src/config/loader.rs:
- Around line 175-191: The merge_configs function currently replaces the entire
base FileConfig whenever the override is non-default, which can unintentionally
reset unrelated fields; modify merge_configs to perform a field-by-field
(shallow) merge: compare each top-level and nested field of override against
FileConfig::default() and for each field choose override's value only when it
differs from the default, otherwise keep base's value (apply this to nested
structs like provider, safety, context, etc., referencing FileConfig and its
nested types such as ProviderConfig and their members like provider.default and
provider.fallback); ensure the function returns a composed FileConfig built from
selected fields rather than returning override wholesale.

In @src/config/merger.rs:
- Around line 117-118: The boolean merge for redact_paths and redact_username
currently uses logical OR (override_config.redact_paths || base.redact_paths)
which prevents higher-priority sources from turning a true value back to false;
change these to use override semantics like confirm_dangerous does by taking the
override value when present and falling back to base (e.g., use the same
unwrap_or/or-override pattern used for confirm_dangerous) so
override_config.redact_paths and override_config.redact_username override
base.redact_paths/base.redact_username rather than OR-ing with them.

In @src/config/paths.rs:
- Around line 52-60: The docstring for get_home_config_path incorrectly states
the return is "~/.config/clai/config.toml" while
directories::BaseDirs::config_dir() returns platform-specific paths; either
update the docstring to describe the platform-specific behavior and keep using
BaseDirs::config_dir(), or implement an explicit Unix fallback that returns
~/.config/clai/config.toml when the OS is Unix (detect via cfg!(unix)) and
BaseDirs::new() returns None; also audit any deduplication logic that assumes
Linux-style paths and update it to handle macOS and Windows config_dir() values
consistently.

In @src/context/directory.rs:
- Around line 124-130: The code incorrectly builds home_var as format!("${}/",
home) (e.g., "$/home/user/") instead of matching the literal "$HOME/"; update
the logic that currently uses std::env::var("HOME") and home_var so it checks
for the literal "$HOME/" prefix and replaces that exact substring: use
redacted.starts_with("$HOME/") and redacted.replacen("$HOME/", "[REDACTED]/", 1)
(remove or ignore the resolved HOME value for this replacement).

In @src/context/stdin.rs:
- Around line 36-58: The current single call to stdin.read(&mut buffer) can
return before EOF and truncate piped input; update the reading logic in the
function that declares max_bytes, buffer and stdin to loop or use a bounded
reader: acquire stdin.lock(), call .take(max_bytes as u64) and then
.read_to_end(&mut buffer) (or loop calling read until EOF or buffer fills) to
reliably read up to max_bytes, preserve the existing error handling and the
conversion via String::from_utf8_lossy(&buffer). Ensure you replace the single
read call and its match with the take+read_to_end (or loop) variant and keep the
same semantics for empty input, successful read, and errors.

In @src/locale/mod.rs:
- Around line 50-92: Tests mutate the LANG env var causing race conditions; make
them isolated by either running tests serially or scoping env changes. Add a
dev-dependency (e.g., serial_test = "3.0") and annotate the tests in the tests
module with the serial attribute to force sequential execution, or add a
dev-dependency (e.g., temp-env = "0.3") and wrap each test's LANG modifications
in a scoped temp_env utility so get_language_code, is_c_locale, and get_locale
see controlled values without affecting other tests. Ensure every test in the
mod tests uses one of these approaches so the environment is restored after each
test.

In @src/main.rs:
- Around line 217-235: The current code reconstructs a clai::cli::Cli by hand
before calling should_prompt, which is verbose and brittle; instead either add a
conversion method like Config::to_cli(&self) -> clai::cli::Cli and call
should_prompt(&config.to_cli(), &file_config), or change should_prompt's
signature to accept &Config (e.g., should_prompt_with_config(&config,
&file_config)) and update its logic to read fields directly (refer to
should_prompt and its usage in handle_cli), or simply pass the original Cli
through to handle_cli so you can call should_prompt with the original Cli; pick
one approach and update the function signature/usages accordingly to eliminate
the manual field mapping.
- Around line 188-206: The dry-run handling block is indented one level deeper
than surrounding code; remove the extra indentation so the if config.dry_run {
... } block aligns with neighboring statements, keeping the same logic (looping
over commands, calling print_command, mapping errors to ClaiError::General,
printing final newline if commands not empty) and preserving references to
config.dry_run, print_command, commands, and ClaiError::General.

In @src/safety/interactive.rs:
- Around line 230-244: The execute_command function currently unconditionally
falls back to "/bin/sh", which breaks on Windows; update execute_command to be
platform-aware: on Unix keep the current logic (use SHELL or "/bin/sh" and run
shell with "-c" and the command), and on Windows use COMSPEC (or "cmd.exe") and
invoke it with "/C" and the command. Implement this via conditional compilation
(#[cfg(unix)] and #[cfg(windows)]) or by branching on std::env::consts::OS,
ensuring you reference execute_command and the Command construction logic so
Windows uses the correct shell and args.

In @test_config.sh:
- Around line 31-131: The script test_config.sh uses a hardcoded project path
(/home/vee/Coding/clAI) which breaks portability; define a single PROJECT_ROOT
(or REPO_DIR) variable near the top (after the shebang) and replace every direct
use of /home/vee/Coding/clAI in the commands and subshells (the cd and cargo
invocations seen in the OUTPUT= and cd lines, and other file ops) with that
variable so all references (used in the OUTPUT= commands, cd commands, and file
creation/removal steps) become relative to PROJECT_ROOT; ensure you quote/expand
the variable correctly and update all occurrences shown in the diff.
- Line 107: The command substitution assigned to OUTPUT contains a duplicated
stderr redirect; update the invocation that sets OUTPUT (the line using cargo r
-- "test") to use a single stderr redirect (e.g., only one 2>&1) so stderr is
not redundantly redirected twice.

In @tests/cli_tests.rs:
- Around line 3-14: The test helper run_clai currently hardcodes the binary path
("./target/debug/clai") which fails when the binary isn't pre-built; change
run_clai to resolve the binary dynamically instead: either use
env!("CARGO_BIN_EXE_clai") (ensure the clai binary is listed in [[bin]] so cargo
test provides that env var) or integrate the escargot crate to build and run the
test binary programmatically; update the Command::new call in run_clai to use
the chosen dynamic path/provider and remove the hardcoded string so tests work
under cargo test and CI.
🟡 Minor comments (21)
CONTRIBUTING.md-11-11 (1)

11-11: Update placeholder repository URL.

Same placeholder URL issue as in README.md. This should be updated to the actual repository path.

🔎 Proposed fix
-git clone https://github.com/yourusername/clAI.git
+git clone https://github.com/Vedaant-Rajoo/clAI.git
README.md-15-15 (1)

15-15: Update placeholder repository URL.

The installation instructions contain a placeholder URL yourusername that should be replaced with the actual repository owner/organization name.

🔎 Proposed fix
-git clone https://github.com/yourusername/clAI.git
+git clone https://github.com/Vedaant-Rajoo/clAI.git
.cargo/config.toml-1-3 (1)

1-3: Documentation inconsistency with alias name.

Line 3 references cargo rb in the example, but the actual alias defined at Line 8 is br (not rb).

🔎 Proposed fix
-# Example: cargo b, cargo r, cargo t
+# Example: cargo b, cargo br, cargo r, cargo t
.taskmaster/tasks/tasks.json-288-293 (1)

288-293: Status inconsistency: subtask marked in-progress but has completion notes.

Subtask 4.2 (Implement OpenRouter Provider) has status "in-progress" but the details section contains extensive completion notes (added on 2026-01-03). Consider updating the status to "done" if the implementation is complete.

.taskmaster/tasks/tasks.json-21-24 (1)

21-24: parentId: "undefined" should be null or omitted.

The string "undefined" appears to be a JavaScript artifact. In JSON, this should be null or the field should be omitted entirely. This pattern repeats throughout the file for all subtasks.

-            "parentId": "undefined"
+            "parentId": null
.cursor/commands/tm/remove-dependency.md-55-62 (1)

55-62: Add language specification to the fenced code block.

Line 57 has a fenced code block without a language identifier. According to markdownlint (MD040), all fenced code blocks should specify a language for proper syntax highlighting and consistency.

🔎 Proposed fix
  ## Example
  
-```
+```bash
  /taskmaster:remove-dependency 5 from 3
  → Removed: Task #5 no longer depends on #3
  → Task #5 is now UNBLOCKED and ready to start
  → Warning: Consider if #5 still needs #2 completed first
</details>

</blockquote></details>
<details>
<summary>.cursor/commands/tm/help.md-84-89 (1)</summary><blockquote>

`84-89`: **Add language specifier to fenced code block.**

The code block is missing a language identifier, which violates the markdownlint MD040 rule. Add a language specifier (`bash`, `text`, or similar) to the opening fence.

<details>
<summary>🔎 Proposed fix</summary>

```diff
-```
+```bash
 /taskmaster:list-tasks
 /taskmaster:show-task 1.2
 /taskmaster:add-task
 /taskmaster:next-task
-```
+```
.cursor/commands/tm/remove-task.md-39-51 (1)

39-51: Add language identifier to fenced code block.

Markdown linting standards require language specification on code blocks for syntax highlighting and consistency.

-   ```
+   ```text
    Task Removal Impact
.cursor/commands/tm/remove-task.md-87-99 (1)

87-99: Add language identifier to fenced code block.

Markdown linting standards require language specification on code blocks for syntax highlighting and consistency.

-```
+```text
 /taskmaster:remove-task 5
.cursor/commands/tm/validate-dependencies.md-45-56 (1)

45-56: Add language specifier to fenced code block.

Line 45 starts a fenced code block showing sample output without specifying a language. Add text to declare it as a plain text block.

🔎 Proposed fix
-```
+```text
 Dependency Validation Report
 ━━━━━━━━━━━━━━━━━━━━━━━━━━
 ✅ No circular dependencies found
 ⚠️  2 warnings found:
    - Task #23 has 7 dependencies (consider breaking down)
    - Task #45 blocks 5 other tasks (potential bottleneck)
 ❌ 1 error found:
    - Task #67 depends on deleted task #66

 Critical Path: #1 → #5 → #23 → #45 → #50 (15 days)
-```
+```
.cursor/commands/tm/install-taskmaster.md-103-110 (1)

103-110: Add language specifier to fenced code block.

Line 103 starts a fenced code block without specifying a language. Add text to declare it as output:

🔎 Proposed fix
-```
+```text
 ✅ Task Master installed
 ✅ Command 'task-master' available globally
 ✅ AI provider configured
 ✅ Ready to use slash commands!

 Try: /taskmaster:init your-prd.md
-```
+```

Per markdownlint rule MD040 (enabled by default), fenced code blocks must include a language identifier.

.taskmaster/docs/prd.txt-1-10 (1)

1-10: Update PRD title from placeholder to "clAI".

Line 2 contains a placeholder title <Unnamed CLI Tool> that should be replaced with the actual product name clAI to align with the PR description and broader documentation.

🔎 Proposed fix
- <Unnamed CLI Tool> PRD v2
+ clAI PRD v2
.cursor/commands/tm/auto-implement-tasks.md-59-68 (1)

59-68: Add language identifier to fenced code block.

The code block showing progressive implementation steps is missing a language specifier. Per Markdown linting standards (MD040), all fenced code blocks should declare a language for proper syntax highlighting and accessibility.

🔎 Proposed fix for fenced code block
-Step-by-step with validation:
-```
+Step-by-step with validation:
+```text
 Step 1/5: Setting up component structure ✓
 Step 2/5: Implementing core logic ✓
 Step 3/5: Adding error handling ⚡ (in progress)
 Step 4/5: Writing tests ⏳
 Step 5/5: Integration testing ⏳
 
 Current: Adding try-catch blocks and validation...
-```
+```
test_config.sh-107-107 (1)

107-107: Fix redundant stderr redirect.

Line 107 has a duplicate 2>&1 redirect, which is redundant.

🔎 Proposed fix
-OUTPUT=$(cargo r -- "test" 2>&1 2>&1)
+OUTPUT=$(cargo r -- "test" 2>&1)
src/config/paths.rs-52-60 (1)

52-60: Cross-platform path inconsistency in documentation and implementation.

The docstring claims this returns ~/.config/clai/config.toml, but directories::BaseDirs::config_dir() returns platform-specific paths:

  • Linux: ~/.config
  • macOS: ~/Library/Application Support
  • Windows: C:\Users\<User>\AppData\Roaming

The comment and deduplication logic assume Linux behavior. Consider updating the documentation or implementing explicit fallback to ~/.config on all Unix platforms.

benches/startup.rs-38-59 (1)

38-59: Benchmark named parse_args does not actually parse arguments.

This benchmark constructs a Cli struct directly rather than parsing command-line arguments with clap. The comment acknowledges this ("create Cli directly (faster than parsing)") but the benchmark name is misleading. If the goal is to measure actual CLI parsing overhead, use Cli::parse_from() or Cli::try_parse_from().

🔎 Suggested fix to benchmark actual parsing
 group.bench_function("parse_args", |b| {
     b.iter(|| {
-        // Simulate parsing CLI args - create Cli directly (faster than parsing)
-        let _cli = Cli {
-            instruction: "list files".to_string(),
-            // ... fields ...
-        };
+        // Actually parse CLI args to measure clap overhead
+        let args = ["clai", "list files"];
+        let _cli = Cli::try_parse_from(black_box(args));
     });
 });
benches/startup.rs-138-143 (1)

138-143: Benchmark does not reflect production usage pattern.

setup_signal_handlers() is called once at startup in production (src/main.rs:24), but the benchmark invokes it repeatedly on each iteration. While signal-hook 0.4 supports multiple registrations for the same signal, the crate documentation explicitly recommends registering handlers early and avoiding frequent registration cycles. This benchmark setup does not represent actual startup behavior and may be measuring unnecessary registration overhead. Consider either measuring this separately with a single iteration, or removing this benchmark if the focus is on representative startup time.

src/context/directory.rs-124-130 (1)

124-130: Bug: $HOME replacement logic is incorrect.

The intent (per documentation) is to replace the literal string $HOME/ in paths. However, the code creates format!("${}/", home) which produces something like $/home/user/ (the resolved HOME value prefixed with $), not the literal $HOME/.

🔎 Proposed fix
     // Replace $HOME/ with [REDACTED]/
-    if let Ok(home) = std::env::var("HOME") {
-        let home_var = format!("${}/", home);
-        if redacted.starts_with(&home_var) {
-            redacted = redacted.replacen(&home_var, "[REDACTED]/", 1);
-        }
-    }
+    if redacted.starts_with("$HOME/") {
+        redacted = redacted.replacen("$HOME/", "[REDACTED]/", 1);
+    } else if redacted == "$HOME" {
+        redacted = "[REDACTED]".to_string();
+    }
src/ai/chain.rs-203-212 (1)

203-212: is_available returns false for unknown providers.

The method only recognizes "openrouter" and returns false for any other provider name. This means if a user configures a fallback chain with providers like "ollama" or future providers, is_available will incorrectly report the chain as unavailable even if openrouter is configured.

Consider returning true for unrecognized providers (optimistic check) or maintaining a registry of known providers.

src/main.rs-188-206 (1)

188-206: Inconsistent indentation in dry_run block.

The code block starting at line 188 has an extra indentation level compared to surrounding code. This appears to be a formatting issue.

src/config/loader.rs-175-191 (1)

175-191: Placeholder merge logic may cause unexpected config overwriting.

The current merge_configs replaces the entire base config if the override is non-default. This means a config file with just [context]\nmax-files = 5 would also reset provider.default, safety.dangerous_patterns, etc. to their defaults.

The comment acknowledges this is a placeholder for subtask 2.4, but this could cause subtle bugs if deployed before proper deep merging is implemented.

🔎 Suggested approach for proper merging

A field-by-field merge would preserve base values when override fields are at defaults:

fn merge_configs(base: FileConfig, over: FileConfig) -> FileConfig {
    let default = FileConfig::default();
    FileConfig {
        provider: ProviderConfig {
            default: if over.provider.default != default.provider.default {
                over.provider.default
            } else {
                base.provider.default
            },
            fallback: if over.provider.fallback != default.provider.fallback {
                over.provider.fallback
            } else {
                base.provider.fallback
            },
        },
        // ... similar for other fields
    }
}

src/main.rs Outdated
Comment on lines 217 to 235
let should_prompt_user = should_prompt(
&clai::cli::Cli {
instruction: config.instruction.clone(),
model: config.model.clone(),
provider: config.provider.clone(),
quiet: config.quiet,
verbose: config.verbose,
no_color: config.no_color,
color: config.color,
interactive: config.interactive,
force: config.force,
dry_run: config.dry_run,
context: config.context.clone(),
offline: config.offline,
num_options: config.num_options,
debug: config.debug,
},
&file_config,
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Reconstructing Cli from Config is verbose and fragile.

This manual reconstruction duplicates the field mapping and can easily drift out of sync if fields are added. Consider:

  1. Adding a method like Config::to_cli(&self) -> Cli
  2. Or passing the original Cli through to handle_cli
  3. Or refactoring should_prompt to accept Config directly
🔎 Suggested approach
// In src/safety/prompt.rs, add an overload or change signature:
pub fn should_prompt_with_config(config: &Config, file_config: &FileConfig) -> bool {
    let is_tty = is_stdin_tty() && is_stdout_tty();
    let confirm_enabled = file_config.safety.confirm_dangerous;
    let force_bypass = config.force;
    is_tty && confirm_enabled && !force_bypass
}
🤖 Prompt for AI Agents
In @src/main.rs around lines 217-235, The current code reconstructs a
clai::cli::Cli by hand before calling should_prompt, which is verbose and
brittle; instead either add a conversion method like Config::to_cli(&self) ->
clai::cli::Cli and call should_prompt(&config.to_cli(), &file_config), or change
should_prompt's signature to accept &Config (e.g.,
should_prompt_with_config(&config, &file_config)) and update its logic to read
fields directly (refer to should_prompt and its usage in handle_cli), or simply
pass the original Cli through to handle_cli so you can call should_prompt with
the original Cli; pick one approach and update the function signature/usages
accordingly to eliminate the manual field mapping.

Comment on lines 3 to 14
fn run_clai(args: &[&str]) -> (String, String, i32) {
let output = Command::new("./target/debug/clai")
.args(args)
.output()
.expect("Failed to execute clai");

let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let exit_code = output.status.code().unwrap_or(-1);

(stdout, stderr, exit_code)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Hardcoded binary path assumes pre-built binary.

The hardcoded path ./target/debug/clai (line 4) will cause test failures if the binary hasn't been built before running tests. This is particularly problematic in CI/CD environments or when running cargo test directly.

🔎 Recommended fix using env::current_exe or cargo test integration

Option 1: Use escargot crate for proper binary compilation in tests:

Add to Cargo.toml:

[dev-dependencies]
escargot = "0.5"

Then modify the helper:

+use escargot::CargoBuild;
+
 fn run_clai(args: &[&str]) -> (String, String, i32) {
-    let output = Command::new("./target/debug/clai")
+    let bin = CargoBuild::new()
+        .bin("clai")
+        .run()
+        .expect("Failed to build clai");
+    
+    let output = Command::new(bin.path())
         .args(args)
         .output()
         .expect("Failed to execute clai");

Option 2: Use env!("CARGO_BIN_EXE_clai") (simpler, works with cargo test):

 fn run_clai(args: &[&str]) -> (String, String, i32) {
-    let output = Command::new("./target/debug/clai")
+    let output = Command::new(env!("CARGO_BIN_EXE_clai"))
         .args(args)
         .output()
         .expect("Failed to execute clai");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn run_clai(args: &[&str]) -> (String, String, i32) {
let output = Command::new("./target/debug/clai")
.args(args)
.output()
.expect("Failed to execute clai");
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let exit_code = output.status.code().unwrap_or(-1);
(stdout, stderr, exit_code)
}
fn run_clai(args: &[&str]) -> (String, String, i32) {
let output = Command::new(env!("CARGO_BIN_EXE_clai"))
.args(args)
.output()
.expect("Failed to execute clai");
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let exit_code = output.status.code().unwrap_or(-1);
(stdout, stderr, exit_code)
}
🤖 Prompt for AI Agents
In @tests/cli_tests.rs around lines 3-14, The test helper run_clai currently
hardcodes the binary path ("./target/debug/clai") which fails when the binary
isn't pre-built; change run_clai to resolve the binary dynamically instead:
either use env!("CARGO_BIN_EXE_clai") (ensure the clai binary is listed in
[[bin]] so cargo test provides that env var) or integrate the escargot crate to
build and run the test binary programmatically; update the Command::new call in
run_clai to use the chosen dynamic path/provider and remove the hardcoded string
so tests work under cargo test and CI.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Fix all issues with AI Agents 🤖
In @src/ai/handler.rs:
- Around line 82-97: Replace the manual reconstruction of Cli by adding a
conversion helper on Config (e.g., implement pub fn to_cli(&self) -> Cli { /*
copy fields: instruction, model, provider, quiet, verbose, no_color, color,
interactive, force, dry_run, context, offline, num_options, debug */ }) and then
replace the block that creates Cli with let cli = config.to_cli(); so future
field changes stay centralized in Config::to_cli and avoid duplicated mapping
logic.

In @src/color/mod.rs:
- Around line 154-169: The test test_detect_color_auto_clicolor mutates global
env vars (std::env::set_var/remove_var) and is race-prone; make it isolated by
either marking the test with a serial lock (e.g., use the serial_test crate and
annotate test_detect_color_auto_clicolor with the serial_test macro) or use a
scoped env helper (e.g., temp-env) to set CLICOLOR only for the test lifetime
while restoring previous state; update the test to use one of these approaches
and remove direct global set_var/remove_var calls, keeping the calls to
detect_color_auto() unchanged.

In @src/config/merger.rs:
- Around line 186-191: The current code silently treats invalid boolean env
values as false by using .parse().unwrap_or(false); change this to explicitly
validate parsing for env.get("context_redact_paths"),
env.get("context_redact_username"), and the safety_confirm_dangerous entry
(merged.safety_confirm_dangerous), e.g., attempt .parse::<bool>() and on Err
emit a clear warning or error that includes the environment key and the invalid
value, then either preserve the existing/default setting or return an error
(choose consistent behavior used elsewhere in merger.rs); apply the same
validated parsing logic to the safety_confirm_dangerous branch as well so typos
like "ture" no longer fail silently.

In @src/context/directory.rs:
- Around line 123-128: The code incorrectly builds home_var with format!("${}/",
home) producing strings like "$/home/user/"; instead, detect and replace the
literal "$HOME/" pattern in the redacted string. In the block that currently
calls std::env::var("HOME") and builds home_var, stop substituting the actual
home value and check redacted.starts_with("$HOME/") (and use
redacted.replacen("$HOME/", "[REDACTED]/", 1)) so the literal placeholder is
recognized and replaced; retain the existing surrounding logic and only change
the pattern construction and replacement target.

In @src/context/gatherer.rs:
- Line 69: get_file_config(&cli).unwrap_or_default() hides file parsing/IO
errors; change it to explicitly handle the Result from get_file_config, log any
Err (including error details) at warn/error level (using the project's logger,
e.g. log::warn or tracing::warn), and still fall back to the default config on
error. Locate the call to get_file_config in gatherer.rs (the file_config
binding) and replace unwrap_or_default() with a match or
.map_err/.unwrap_or_else pattern that logs the error and returns
Default::default() when necessary.
- Around line 53-69: The manual field-by-field construction of Cli in
gatherer.rs is brittle; implement a conversion (e.g., impl From<&Config> for Cli
or a Config::to_cli()/Cli::from_config(&Config) helper) that maps Config -> Cli,
then replace the block that builds Cli with a single call like let cli =
Cli::from(&config) (or let cli = config.to_cli()), keeping the subsequent
get_file_config(&cli).unwrap_or_default() usage unchanged; ensure all fields
currently copied (instruction, model, provider, quiet, verbose, no_color, color,
interactive, force, dry_run, context, offline, num_options, debug) are covered
by the conversion.

In @src/safety/patterns.rs:
- Around line 56-69: The code currently logs a "Warning" then returns an Err,
which is inconsistent; choose the fail-fast approach (recommended): remove the
eprintln! call and any "Warning" logging around the regex compilation failure
inside the loop that iterates dangerous_patterns, and simply return the
anyhow::Error (the existing Err(...) with .context(...)) when compiling pattern
fails (use the pattern, index, and e as in the existing Err). Ensure compiled
collection logic for successful patterns remains unchanged so an invalid regex
causes immediate failure in the function that compiles dangerous_patterns.
♻️ Duplicate comments (6)
src/locale/mod.rs (1)

49-91: Test isolation issue already flagged in previous review.

The environment variable mutation issue in these tests was already identified in a previous review. Please refer to the earlier comment which suggested using either serial_test or temp-env crate to prevent race conditions.

src/context/stdin.rs (1)

36-58: Single read() call may truncate stdin data.

This issue was previously identified: a single stdin.read(&mut buffer) call can return fewer bytes than available, potentially truncating piped input. The suggested fix using Take and read_to_end remains valid and should be implemented.

src/main.rs (1)

217-234: Reconstructing Cli from Config is verbose and fragile.

This manual reconstruction duplicates field mapping and can drift out of sync if fields are added or changed. The past review suggestion remains valid: either add a Config::to_cli(&self) -> Cli method, pass the original Cli through to handle_cli, or refactor should_prompt to accept Config directly.

src/safety/interactive.rs (1)

236-250: Windows compatibility: /bin/sh fallback will fail on Windows.

The fallback shell /bin/sh doesn't exist on Windows. The code lacks platform-specific handling that exists elsewhere in the codebase (e.g., src/config/loader.rs uses #[cfg(unix)]). Consider adding Windows-specific shell detection using cmd.exe /C or use conditional compilation to handle both platforms.

Run the following script to check for existing platform-specific patterns in the codebase:

#!/bin/bash
# Check for existing Windows/Unix conditional compilation patterns
echo "=== Checking for cfg(unix) and cfg(windows) usage ==="
rg -n "#\[cfg\((?:unix|windows)\)\]" --type rust

echo ""
echo "=== Checking for SHELL environment variable usage ==="
rg -n 'env::var.*"SHELL"' --type rust

echo ""
echo "=== Checking for cmd.exe or powershell references ==="
rg -n -i "cmd\.exe|powershell" --type rust
src/config/file.rs (1)

114-123: Two distinct default_dangerous_patterns() functions with mismatched content must be reconciled.

The default_dangerous_patterns() in this file returns simple substrings ("rm -rf", "sudo rm"), while src/safety/patterns.rs defines regex patterns (r"rm\s+-rf\s+/", r"sudo\s+rm\s+-rf\s+/"). The patterns also differ in coverage—only patterns.rs includes Windows-specific patterns (del, format C:).

If config patterns are compiled as regex downstream, users editing the config based on these defaults will expect substring matching but get regex behavior instead. Reconcile by either:

  1. Converting these defaults to match the regex strings in patterns.rs, or
  2. Centralizing the canonical list in one location and importing it
src/ai/providers/openrouter.rs (1)

118-334: Remove hardcoded debug logging before merge.

The debug logging writes to a hardcoded absolute path (/home/vee/Coding/clAI/.cursor/debug.log) which is developer-specific. This appears to be Cursor AI agent debugging code that should be removed for production.

🧹 Nitpick comments (7)
src/safety/patterns.rs (2)

16-27: Consider removing redundant pattern.

Lines 18-19 define two patterns for "rm -rf /":

  • Line 18: r"rm\s+-rf\s+/" matches "rm -rf /" anywhere in the command
  • Line 19: r"rm\s+-rf\s+/\s*$" matches "rm -rf /" at the end

The first pattern subsumes the second, making line 19 redundant.

🔎 Proposed simplification
 fn default_dangerous_patterns() -> Vec<String> {
     vec![
         r"rm\s+-rf\s+/".to_string(),             // rm -rf /
-        r"rm\s+-rf\s+/\s*$".to_string(),         // rm -rf / (end of line)
         r"dd\s+if=/dev/zero".to_string(),        // dd if=/dev/zero
         // ... rest
     ]
 }

104-108: Consider removing no-op function.

This function does nothing and exists only to document the OnceLock limitation. A comment in the test module would be clearer than a no-op function.

🔎 Proposed alternative

Remove the function and add a comment in the test module:

-/// Reset dangerous regex cache (for testing only)
-///
-/// # Safety
-/// This function is only intended for testing.
-/// It clears the cache, allowing tests to use different configs.
-#[cfg(test)]
-pub fn reset_regex_cache() {
-    // OnceLock doesn't have a reset method, so we can't actually reset it
-    // This is a no-op, but documents the intent for testing
-    // In practice, tests should use different configs or test compile_dangerous_regexes directly
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
     use crate::config::file::FileConfig;
+    
+    // Note: DANGEROUS_REGEXES uses OnceLock and cannot be reset between tests.
+    // Tests should use compile_dangerous_regexes() directly to avoid cache issues.
src/logging/mod.rs (1)

45-66: Consider reducing duplication in format_log.

The colored and non-colored branches duplicate the prefix logic. You could extract a helper function that returns the prefix string, then apply colorization conditionally.

🔎 Proposed refactor to reduce duplication
 pub fn format_log(level: LogLevel, message: &str, color_mode: ColorMode) -> String {
     let use_color = color_mode.should_use_color();
+    
+    let prefix = match level {
+        LogLevel::Error => "ERROR",
+        LogLevel::Warning => "WARN",
+        LogLevel::Info => "INFO",
+        LogLevel::Debug => "DEBUG",
+        LogLevel::Trace => "TRACE",
+    };
+    
+    let colored_prefix = if use_color {
+        match level {
+            LogLevel::Error => colorize(prefix, "red"),
+            LogLevel::Warning => colorize(prefix, "yellow"),
+            LogLevel::Info => colorize(prefix, "blue"),
+            LogLevel::Debug => colorize(prefix, "cyan"),
+            LogLevel::Trace => colorize(prefix, "magenta"),
+        }
+    } else {
+        prefix.to_string()
+    };

-    if use_color {
-        match level {
-            LogLevel::Error => format!("{} {}", colorize("ERROR", "red"), message),
-            LogLevel::Warning => format!("{} {}", colorize("WARN", "yellow"), message),
-            LogLevel::Info => format!("{} {}", colorize("INFO", "blue"), message),
-            LogLevel::Debug => format!("{} {}", colorize("DEBUG", "cyan"), message),
-            LogLevel::Trace => format!("{} {}", colorize("TRACE", "magenta"), message),
-        }
-    } else {
-        // No color - just prefix with level
-        match level {
-            LogLevel::Error => format!("ERROR: {}", message),
-            LogLevel::Warning => format!("WARN: {}", message),
-            LogLevel::Info => format!("INFO: {}", message),
-            LogLevel::Debug => format!("DEBUG: {}", message),
-            LogLevel::Trace => format!("TRACE: {}", message),
-        }
-    }
+    if use_color {
+        format!("{} {}", colored_prefix, message)
+    } else {
+        format!("{}: {}", colored_prefix, message)
+    }
 }
src/error/mod.rs (1)

49-114: LGTM! Error handling implementation is solid.

The exit_code mapping, stderr printing, and backtrace extraction are all well-implemented.

Minor nitpick at line 106: prefer !backtrace_str.is_empty() over backtrace_str.len() > 0 for idiomatic Rust.

src/config/file.rs (1)

219-219: Replace magic number with dynamic length check.

The test hardcodes assert_eq!(config.safety.dangerous_patterns.len(), 6); which breaks if default patterns are added/removed.

🔎 Proposed fix
-        assert_eq!(config.safety.dangerous_patterns.len(), 6);
+        assert!(!config.safety.dangerous_patterns.is_empty());

Or verify specific patterns instead:

-        assert_eq!(config.safety.dangerous_patterns.len(), 6);
+        assert_eq!(config.safety.dangerous_patterns, default_dangerous_patterns());
src/ai/providers/openrouter.rs (2)

38-41: Avoid expect() panic in public constructor.

The .expect("Failed to create HTTP client") will panic if the HTTP client cannot be built. For a library, constructors should return Result to allow callers to handle errors gracefully.

🔎 Proposed fix

Change the signature and implementation:

-    pub fn new(api_key: String, default_model: Option<String>) -> Self {
+    pub fn new(api_key: String, default_model: Option<String>) -> Result<Self> {
         let client = Client::builder()
             .timeout(Duration::from_secs(60))
             .build()
-            .expect("Failed to create HTTP client");
+            .context("Failed to create HTTP client")?;

-        Self {
+        Ok(Self {
             client,
             api_key,
             default_model,
-        }
+        })
     }

Update callers to handle the Result.


100-113: Fragile retry logic relies on string matching.

Line 104 checks e.to_string().contains("429") to detect rate-limit errors, which is brittle—it depends on error message formatting and may miss structured errors or catch false positives.

🔎 Proposed fix

Return structured error information from make_request:

// Define a custom error type
enum OpenRouterError {
    RateLimit { retry_after: Option<u64> },
    Network(anyhow::Error),
    Api(u16, String),
    // ...
}

// In make_request, return OpenRouterError instead of anyhow::Error
// Then in make_request_with_retry:
match self.make_request(&request).await {
    Ok(response) => return Ok(response),
    Err(OpenRouterError::RateLimit { .. }) if retries > 0 => {
        retries -= 1;
        tokio::time::sleep(delay).await;
        delay *= 2;
        continue;
    }
    Err(e) => return Err(e.into()),
}

Alternatively, inspect reqwest::StatusCode directly before converting to string errors.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 05b6fc5 and 18d4700.

📒 Files selected for processing (33)
  • examples/test_context.rs
  • src/ai/chain.rs
  • src/ai/handler.rs
  • src/ai/mod.rs
  • src/ai/prompt.rs
  • src/ai/providers/mod.rs
  • src/ai/providers/openrouter.rs
  • src/ai/types.rs
  • src/cli/mod.rs
  • src/color/mod.rs
  • src/config/file.rs
  • src/config/merger.rs
  • src/config/mod.rs
  • src/config/paths.rs
  • src/context/directory.rs
  • src/context/gatherer.rs
  • src/context/mod.rs
  • src/context/stdin.rs
  • src/context/system.rs
  • src/error/mod.rs
  • src/lib.rs
  • src/locale/mod.rs
  • src/logging/mod.rs
  • src/main.rs
  • src/output/mod.rs
  • src/safety/detector.rs
  • src/safety/interactive.rs
  • src/safety/mod.rs
  • src/safety/patterns.rs
  • src/safety/prompt.rs
  • src/signals/mod.rs
  • tests/cli_tests.rs
  • tests/test_context_gathering.rs
🚧 Files skipped from review as they are similar to previous changes (8)
  • tests/cli_tests.rs
  • src/ai/chain.rs
  • src/output/mod.rs
  • src/config/paths.rs
  • src/ai/types.rs
  • src/signals/mod.rs
  • src/context/mod.rs
  • src/context/system.rs
🧰 Additional context used
🧬 Code graph analysis (17)
src/safety/patterns.rs (1)
src/config/file.rs (7)
  • default_dangerous_patterns (114-123)
  • default (136-157)
  • default (162-167)
  • default (171-178)
  • default (182-187)
  • default (191-195)
  • default (199-205)
src/cli/mod.rs (2)
src/ai/chain.rs (1)
  • name (196-198)
src/ai/providers/openrouter.rs (1)
  • name (369-371)
src/safety/detector.rs (2)
src/safety/patterns.rs (1)
  • get_dangerous_regexes (87-96)
src/config/file.rs (6)
  • default (136-157)
  • default (162-167)
  • default (171-178)
  • default (182-187)
  • default (191-195)
  • default (199-205)
src/context/gatherer.rs (4)
src/context/directory.rs (2)
  • scan_directory (18-61)
  • redact_path_internal (104-139)
src/context/history.rs (1)
  • get_shell_history (116-123)
src/context/stdin.rs (1)
  • read_stdin_default (66-69)
src/context/system.rs (1)
  • get_formatted_system_info (119-122)
src/safety/prompt.rs (2)
src/signals/mod.rs (2)
  • is_stdin_tty (68-70)
  • is_stdout_tty (62-64)
src/config/file.rs (6)
  • default (136-157)
  • default (162-167)
  • default (171-178)
  • default (182-187)
  • default (191-195)
  • default (199-205)
src/logging/mod.rs (1)
src/color/mod.rs (1)
  • color_mode_from_config (68-80)
src/config/mod.rs (2)
src/config/cache.rs (1)
  • get_file_config (29-45)
src/config/file.rs (1)
  • test_config_clone (247-253)
src/safety/mod.rs (5)
src/safety/confirmation.rs (2)
  • format_decision (151-157)
  • handle_dangerous_confirmation (74-140)
src/safety/detector.rs (3)
  • get_matching_pattern (79-96)
  • is_dangerous_command (26-39)
  • is_dangerous_command_with_regexes (64-66)
src/safety/interactive.rs (2)
  • execute_command (236-250)
  • prompt_command_action (86-224)
src/safety/patterns.rs (2)
  • compile_dangerous_regexes (42-75)
  • get_dangerous_regexes (87-96)
src/safety/prompt.rs (3)
  • is_interactive_mode (50-52)
  • is_piped_output (60-62)
  • should_prompt (30-42)
src/safety/interactive.rs (2)
src/signals/mod.rs (1)
  • is_stderr_tty (74-76)
src/config/mod.rs (1)
  • from_cli (29-57)
src/config/merger.rs (2)
src/config/loader.rs (1)
  • load_all_configs (134-173)
src/config/file.rs (6)
  • default (136-157)
  • default (162-167)
  • default (171-178)
  • default (182-187)
  • default (191-195)
  • default (199-205)
examples/test_context.rs (1)
src/context/gatherer.rs (1)
  • gather_context (41-98)
src/ai/providers/openrouter.rs (2)
src/ai/chain.rs (4)
  • new (29-44)
  • complete (158-194)
  • name (196-198)
  • is_available (200-209)
src/ai/types.rs (4)
  • new (41-43)
  • new (114-121)
  • new (203-209)
  • system (54-56)
src/ai/handler.rs (4)
src/ai/prompt.rs (8)
  • build_chat_request (119-132)
  • build_multi_chat_request (148-182)
  • build_prompt (44-80)
  • extract_command (95-105)
  • extract_commands (196-277)
  • serde_json (215-215)
  • serde_json (237-237)
  • serde_json (252-252)
src/config/cache.rs (1)
  • get_file_config (29-45)
src/context/gatherer.rs (1)
  • gather_context (41-98)
src/ai/chain.rs (1)
  • new (29-44)
src/config/file.rs (1)
src/safety/patterns.rs (1)
  • default_dangerous_patterns (16-27)
src/ai/prompt.rs (1)
src/ai/types.rs (5)
  • new (41-43)
  • new (114-121)
  • new (203-209)
  • system (54-56)
  • user (67-69)
src/main.rs (7)
src/cli/mod.rs (1)
  • parse_args (81-83)
src/output/mod.rs (1)
  • print_command (24-36)
src/safety/interactive.rs (1)
  • execute_command (236-250)
src/safety/confirmation.rs (1)
  • handle_dangerous_confirmation (74-140)
src/safety/detector.rs (1)
  • is_dangerous_command (26-39)
src/safety/prompt.rs (2)
  • should_prompt (30-42)
  • is_interactive_mode (50-52)
src/signals/mod.rs (3)
  • is_interactive (81-83)
  • is_interrupted (56-58)
  • setup_signal_handlers (29-52)
tests/test_context_gathering.rs (2)
src/context/gatherer.rs (1)
  • gather_context (41-98)
examples/test_context.rs (1)
  • serde_json (37-37)
🔇 Additional comments (37)
src/locale/mod.rs (1)

1-47: LGTM! Clean locale utilities.

The locale detection functions are well-structured, pure, and properly documented. The logic for extracting language codes handles various locale formats correctly (e.g., "en_US.UTF-8" → "en", "C" → "C").

src/ai/providers/mod.rs (1)

1-3: LGTM! Standard module pattern.

Clean module declaration and re-export following Rust conventions.

tests/test_context_gathering.rs (1)

1-81: LGTM! Comprehensive integration test.

The test thoroughly validates the JSON structure and required fields of the gathered context. The use of actual system state is appropriate for an integration test.

examples/test_context.rs (1)

1-105: LGTM! Clear and educational example.

The example effectively demonstrates how to use gather_context with proper error handling and result display. The structured summary output makes it easy to understand the gathered data.

src/color/mod.rs (1)

1-80: LGTM! Well-designed color detection system.

The implementation follows established standards (GNU CLICOLOR, no-color.org, POSIX) with clear priority ordering. The pure function design and comprehensive documentation are excellent.

src/safety/prompt.rs (1)

30-42: LGTM!

The prompt decision logic correctly combines TTY state, config settings, and force flag. Clean implementation using existing helper functions from the signals module.

src/cli/mod.rs (1)

1-83: LGTM!

Clean CLI definition using Clap with comprehensive options. The deprecation notice for --no-color in favor of --color is a good practice for API evolution.

src/ai/handler.rs (1)

135-169: LGTM!

The orchestration flow is well-structured: context gathering → prompt building → provider invocation → command extraction. Debug output is appropriately gated.

src/safety/mod.rs (1)

1-13: LGTM! Clean module organization.

The safety module aggregates and re-exports the submodule APIs clearly, providing a unified public interface for the safety subsystem.

src/safety/detector.rs (3)

26-39: LGTM! Fail-safe error handling.

The function correctly returns true on regex compilation failure, ensuring dangerous commands are blocked when pattern validation fails. This is the right security posture.


64-66: LGTM!

Clean, pure function for checking commands against pre-compiled regexes.


79-96: LGTM! Safe fallback for pattern retrieval.

The function safely handles missing patterns with unwrap_or_else, though note that the OnceLock caching design in patterns.rs means the index/pattern mapping is fixed after first initialization.

src/context/gatherer.rs (2)

111-129: LGTM! Clean JSON formatting.

The function correctly handles optional stdin and provides good error context.


140-153: LGTM! Safe error handling.

The fallback error JSON ensures callers always receive valid JSON, even on failure.

src/config/merger.rs (4)

16-32: LGTM! Clear precedence chain.

The merge flow correctly implements the documented precedence: Defaults → Files → Env → CLI.


43-56: LGTM! Clean environment variable extraction.


217-247: LGTM! CLI override logic is sound.

The function correctly handles both existing and new provider configurations.


249-359: LGTM! Comprehensive test coverage.

Tests validate all major merge paths and precedence behavior.

src/logging/mod.rs (2)

4-40: LGTM! Well-designed log level abstraction.

The LogLevel enum is cleanly designed with proper trait derivations. The pure function approach for level derivation is excellent, and the documentation clearly explains the behavior.


83-140: LGTM! Logger implementation follows best practices.

The Logger struct cleanly separates pure functions (should_log, format_message) from side effects (log method). The convenience methods provide a good API surface.

src/main.rs (4)

21-67: LGTM! Well-structured main entry point.

The main function properly orchestrates signal handling, error handling, and exit codes. The extract_status_code helper correctly uses lazy regex initialization for performance.


69-116: LGTM! Robust configuration loading with sensible fallbacks.

The run_main function handles configuration loading elegantly, treating missing config files as non-fatal while properly propagating parse errors. The error mapping to appropriate ClaiError variants is correct.


118-212: LGTM! Well-orchestrated CLI handling with proper I/O separation.

The function correctly initializes logging, generates commands with appropriate error mapping, and handles dry-run mode. The stdout/stderr separation is properly maintained throughout.


214-329: Complex but well-structured safety and interactive flow.

The safety flow properly handles dangerous commands, interactive prompts, and safe execution paths. The error mapping to ClaiError variants is comprehensive, and fallback behaviors are sensible.

src/ai/prompt.rs (5)

6-29: LGTM! Solid regex pattern for command extraction.

The COMMAND_EXTRACTION_REGEX is well-designed to handle various markdown fence formats. The (?s) flag and non-greedy capture ensure correct matching of multiline commands.


31-105: LGTM! Clean prompt construction and extraction logic.

The build_prompt function logically structures the context, and extract_command properly handles both markdown-fenced and plain text responses with sensible fallback behavior.


107-182: LGTM! Well-structured chat request builders.

Both single and multi-command request builders properly construct ChatMessage sequences with clear system prompts. The multi-command prompt provides explicit JSON format instructions to guide the AI.


184-277: LGTM! Robust command extraction with excellent fallback handling.

The extract_commands function gracefully handles multiple response formats with progressive fallbacks. Empty command filtering and comprehensive error messages make this production-ready.


279-478: Excellent test coverage!

The test suite comprehensively covers all extraction scenarios, prompt building, and edge cases. This gives high confidence in the module's behavior.

src/error/mod.rs (2)

1-47: LGTM! Well-designed error enum with comprehensive coverage.

The ClaiError enum covers all major failure modes with appropriate exit codes. The API variant's optional status_code field is a nice touch for better diagnostics.


117-187: LGTM! Proper error conversions with good test coverage.

The From implementations correctly map external errors to appropriate ClaiError variants. Tests validate all key behaviors including exit codes and conversions.

src/safety/interactive.rs (3)

10-48: LGTM! Well-designed action and error types.

The CommandAction and InteractiveError enums cover all necessary cases with clear semantics. The Display implementation provides user-friendly error messages.


49-224: LGTM! Excellent interactive prompt implementation.

The prompt_command_action function is well-architected with proper TTY checking, raw mode management, and inline Tab cycling. The cleanup logic ensures raw mode is disabled even on errors.


252-293: Good basic test coverage with acknowledged limitations.

The tests cover essential execute_command scenarios and validate the NoCommands error case. The note about manual testing for TTY interaction is appropriate, as automated testing of terminal interactions is complex.

src/config/mod.rs (1)

1-183: LGTM!

The Config::from_cli implementation is clean and correct:

  • num_options clamping ensures valid range [1, 10]
  • --no-color properly overrides --color
  • Pure transformation with no side effects
  • Comprehensive test coverage for immutability, cloning, and boundary cases
src/lib.rs (1)

1-32: LGTM!

The public API surface is well-organized with clear module boundaries and selective re-exports for convenience. No duplicate exports detected.

src/ai/mod.rs (1)

1-16: LGTM!

Clean module organization with well-structured re-exports that form a coherent public API surface for the AI subsystem.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Fix all issues with AI Agents
In @benches/startup.rs:
- Around line 142-147: The benchmark repeatedly registers signal handlers by
calling setup_signal_handlers() inside b.iter(), causing accumulation of
signal_hook::flag::register() handlers and unmanaged SigId values; to fix,
remove the call from the per-iteration loop and call setup_signal_handlers()
once before group.bench_function (or delete this benchmark entirely) so handler
registration mirrors real-world startup usage and prevents repeated SigId
accumulation.

In @CONTRIBUTING.md:
- Around line 27-28: The fenced code block that lists the project structure
currently lacks a language identifier; update the block around the line
containing "src/" to use a language tag (e.g., change ``` to ```text) so the
snippet is rendered correctly (locate the fenced code block in CONTRIBUTING.md
that contains the single line "src/" and add the language identifier).
- Around line 62-65: Update the config file precedence list in CONTRIBUTING.md
to match the implementation that supports XDG_CONFIG_HOME: replace the
three-item list with a four-item ordered list showing (1) ./ .clai.toml
(project-local, highest), (2) $XDG_CONFIG_HOME/clai/config.toml (if
XDG_CONFIG_HOME is set), (3) ~/.config/clai/config.toml (fallback if
XDG_CONFIG_HOME not set), and (4) /etc/clai/config.toml (system, lowest) so it
aligns with the behavior implemented in src/config/paths.rs.

In @scripts/hooks/pre-commit:
- Around line 9-10: Replace the direct formatting run in the pre-commit hook so
it fails rather than mutates files: in scripts/hooks/pre-commit change the plain
`cargo fmt` invocation to a check-only invocation such as `cargo fmt --all --
--check` (or `cargo fmt -- --check` if you only want the current crate) so the
hook will error when formatting is required and force the developer to run
`cargo fmt` manually before committing.

In @src/config/merger.rs:
- Around line 117-118: The merge currently uses boolean OR which prevents
higher-priority sources from unsetting values; change the merge for redact_paths
and redact_username to take the override value when present, falling back to
base otherwise (e.g. replace "override_config.redact_paths || base.redact_paths"
with "override_config.redact_paths.unwrap_or(base.redact_paths)" and do the same
for redact_username), ensuring you use the Option-style override semantics from
override_config over base in merger.rs.

In @src/main.rs:
- Around line 219-224: The extra unconditional println!() after the dry-run
block can produce a double newline because print_command already emits a
trailing newline for TTYs; in main where you check commands.is_empty(), either
remove that println!() entirely or wrap it so it only prints when stdout is not
a TTY (reuse the same TTY check used by print_command in src/output/mod.rs),
ensuring you avoid emitting two newlines for terminal output while preserving a
final newline for non-TTY outputs.

In @test_config.sh:
- Line 107: Remove the duplicated stderr redirect in the OUTPUT assignment for
the cargo run invocation: in the line assigning OUTPUT (the command invoking
cargo r -- "test"), eliminate the extra "2>&1" so stderr is redirected only once
so OUTPUT captures stdout and stderr correctly.
- Around line 19-27: The test_result function is reading $? inside itself which
sees the if’s test rather than the previous command; change test_result to
accept the exit status as its first argument (e.g., use a local status variable
and test that) and then update all callers to pass "$?" as the first parameter
when invoking test_result so the function evaluates the correct command result
and increments TESTS_PASSED/TESTS_FAILED accordingly.
🧹 Nitpick comments (16)
Cargo.toml (1)

5-5: Update placeholder author information.

The authors field contains placeholder text that should be updated with actual maintainer information before release.

🔎 Proposed fix
-authors = ["Your Name <you@example.com>"]
+authors = ["Vedaant Rajoo <vedaant@example.com>"]
scripts/pre-commit.sh (1)

9-10: Inconsistent formatting behavior with lint.sh.

This script runs cargo fmt which modifies files, while scripts/lint.sh uses cargo fmt -- --check which only verifies formatting. Consider clarifying the intent:

  • If this is meant to auto-fix before committing, the current behavior is fine but the comment should reflect that.
  • If this is meant to check/validate (like lint.sh), use --check.
🔎 Option A: Update comment to clarify auto-fix behavior
-# Run this script before committing to ensure code quality
+# Run this script before committing to auto-fix formatting and ensure code quality
🔎 Option B: Use --check for consistency with lint.sh
 echo "1. Formatting code..."
-cargo fmt
+cargo fmt -- --check
scripts/install-hooks.sh (1)

17-24: Consider using symlink instead of copy for easier hook updates.

Copying the hook file means developers need to re-run this script whenever the hook is updated. Using a symbolic link ensures they always get the latest version.

🔎 Proposed fix using symlink
 # Install pre-commit hook
 if [ -f "$SCRIPTS_HOOKS_DIR/pre-commit" ]; then
     echo "Installing pre-commit hook..."
-    cp "$SCRIPTS_HOOKS_DIR/pre-commit" "$HOOKS_DIR/pre-commit"
-    chmod +x "$HOOKS_DIR/pre-commit"
+    ln -sf "$SCRIPTS_HOOKS_DIR/pre-commit" "$HOOKS_DIR/pre-commit"
     echo "✓ Pre-commit hook installed"
 else
     echo "✗ Warning: scripts/hooks/pre-commit not found"
 fi
src/output/mod.rs (1)

49-75: Consider using Config::default() with field overrides for test maintainability.

Manually constructing Config with all fields is fragile—if new fields are added, all tests will break. If Config implements Default, use it with struct update syntax.

🔎 Proposed refactor (if Config implements Default)
     #[test]
     fn test_format_output_pure() {
         let config = Config {
             instruction: "test instruction".to_string(),
-            model: None,
-            provider: None,
-            quiet: false,
-            verbose: 0,
-            no_color: false,
-            color: crate::cli::ColorChoice::Auto,
-            interactive: false,
-            force: false,
-            dry_run: false,
-            context: None,
-            offline: false,
-            num_options: 3,
-            debug: false,
-            debug_log_file: None,
+            ..Config::default()
         };
benches/startup.rs (2)

39-60: Benchmark "parse_args" does not actually benchmark argument parsing.

The comment says "Simulate parsing CLI args" and the code constructs a Cli struct directly. This measures struct allocation, not Cli::try_parse(). If the goal is to benchmark actual parsing, use Cli::try_parse_from(["clai", "test instruction"]).

🔎 Proposed fix to benchmark actual parsing
     // Benchmark: CLI parsing
     group.bench_function("parse_args", |b| {
         b.iter(|| {
-            // Simulate parsing CLI args - create Cli directly (faster than parsing)
-            let _cli = Cli {
-                instruction: "list files".to_string(),
-                model: None,
-                provider: None,
-                quiet: false,
-                verbose: 0,
-                no_color: false,
-                color: clai::cli::ColorChoice::Auto,
-                interactive: false,
-                force: false,
-                dry_run: false,
-                context: None,
-                offline: false,
-                num_options: 3,
-                debug: false,
-                debug_file: None,
-            };
+            // Actually parse CLI args using clap
+            let _cli = clai::cli::Cli::try_parse_from(black_box(["clai", "list files"]));
         });
     });

316-318: Explicit drop(temp_file) is redundant.

The NamedTempFile will be automatically dropped when benchmark_history_reading returns, cleaning up the temporary file. The explicit drop adds no value here.

🔎 Proposed fix
-    // Cleanup
-    drop(temp_file);
 }
src/cli/mod.rs (1)

41-47: Consider deprecation warning or mutual exclusivity for --no-color and --color.

--no-color is documented as deprecated in favor of --color=never, but both flags are still accepted. If a user passes --no-color --color=always, the behavior depends on processing order. Consider either:

  1. Adding conflicts_with = "color" to prevent confusion
  2. Emitting a deprecation warning when --no-color is used
🔎 Proposed fix to add conflict
     /// Disable colored output (deprecated: use --color=never)
-    #[arg(long = "no-color")]
+    #[arg(long = "no-color", conflicts_with = "color", hide = true)]
     pub no_color: bool,
src/ai/chain.rs (1)

93-94: Mutex .unwrap() could panic on poisoned lock.

If a previous thread panicked while holding the lock, subsequent .unwrap() calls will panic. Consider using .lock().expect("message") or handling the poison case gracefully.

🔎 Proposed fix
     fn get_provider(&self, index: usize) -> Result<Arc<dyn Provider>> {
-        let mut instances = self.provider_instances.lock().unwrap();
+        let mut instances = self.provider_instances
+            .lock()
+            .map_err(|_| anyhow::anyhow!("Provider instance cache lock poisoned"))?;
src/ai/handler.rs (1)

146-161: Extract duplicated debug output into a helper function.

The debug output blocks in generate_command (lines 147-161) and generate_commands (lines 201-216) are nearly identical. Consider extracting to a helper:

🔎 Proposed helper function
fn debug_print_request(request: &ChatRequest, num_options: Option<u8>) {
    eprintln!("\n=== DEBUG: Request to be sent to AI ===");
    eprintln!("Model: {:?}", request.model);
    eprintln!("Temperature: {:?}", request.temperature);
    eprintln!("Max Tokens: {:?}", request.max_tokens);
    if let Some(n) = num_options {
        eprintln!("Number of options requested: {}", n);
    }
    eprintln!("\nMessages:");
    for (i, msg) in request.messages.iter().enumerate() {
        eprintln!("  {}. Role: {:?}", i + 1, msg.role);
        eprintln!("     Content: {}", msg.content);
        if i < request.messages.len() - 1 {
            eprintln!();
        }
    }
    eprintln!("=== END DEBUG ===\n");
}

Also applies to: 200-216

src/error/mod.rs (2)

139-143: Use .is_empty() instead of .len() > 0.

This is more idiomatic Rust and would be flagged by Clippy.

🔎 Proposed fix
-                if backtrace_str.len() > 0 {
+                if !backtrace_str.is_empty() {
                     Some(backtrace_str)
                 } else {
                     None
                 }

120-147: Consider renaming backtrace() to error_chain() for clarity.

This method returns the chain of error causes (via .source()), not an actual stack backtrace. The name backtrace may mislead developers expecting std::backtrace::Backtrace.

src/main.rs (1)

47-49: Re-parsing CLI args just for verbosity is wasteful.

When an error occurs, the code parses CLI args again just to extract the verbosity level. This duplicates work and could fail differently than the original parse. Consider storing verbosity in a thread-local or passing it through the error path.

🔎 Alternative approach
 async fn main() {
     // Setup signal handlers early (SIGINT, SIGTERM, SIGPIPE)
     let interrupt_flag = setup_signal_handlers();
+    
+    // Parse CLI once upfront for verbosity (used in error handling)
+    let verbose = parse_args().map(|cli| cli.verbose).unwrap_or(0);

     // Check for interruption before starting
     if is_interrupted(&interrupt_flag) {
         process::exit(ExitCode::Interrupted.as_i32());
     }

     // Function composition: parse_args() |> build_config() |> handle_cli()
-    let result = run_main(&interrupt_flag).await;
+    let result = run_main(&interrupt_flag, verbose).await;
     
     // ... later in error handling ...
-            let verbose = parse_args().map(|cli| cli.verbose).unwrap_or(0);
             err.print_stderr(verbose);
src/logging/file_logger.rs (1)

98-106: Consider using a more direct role conversion.

Line 102 uses format!("{:?}", m.role).to_lowercase() to convert the Role enum to a string. This relies on the Debug representation being lowercase-compatible, which is fragile.

🔎 More robust role conversion approach

Consider adding a method to the Role enum for direct string conversion, or use match:

 let messages_data: Vec<serde_json::Value> = messages
     .iter()
     .map(|m| {
         serde_json::json!({
-            "role": format!("{:?}", m.role).to_lowercase(),
+            "role": match m.role {
+                crate::ai::types::Role::System => "system",
+                crate::ai::types::Role::User => "user",
+                crate::ai::types::Role::Assistant => "assistant",
+            },
             "content": m.content
         })
     })
     .collect();

Or add a method to Role:

impl Role {
    pub fn as_str(&self) -> &'static str {
        match self {
            Role::System => "system",
            Role::User => "user",
            Role::Assistant => "assistant",
        }
    }
}
src/config/mod.rs (1)

83-91: Consider handling bare ~ in path expansion.

The expand_path method only handles ~/ prefix but not bare ~. While ~/ is the common case, supporting bare ~ would be more robust.

🔎 Enhanced path expansion
 fn expand_path(path: &str) -> PathBuf {
+    if path == "~" {
+        if let Some(base_dirs) = directories::BaseDirs::new() {
+            return base_dirs.home_dir().to_path_buf();
+        }
+    }
     if let Some(stripped) = path.strip_prefix("~/") {
         if let Some(base_dirs) = directories::BaseDirs::new() {
             return base_dirs.home_dir().join(stripped);
         }
     }
     PathBuf::from(path)
 }
src/ai/providers/openrouter.rs (2)

55-66: Consider returning Result from constructor instead of panicking.

Line 59 uses expect() which will panic if the HTTP client fails to build. While rare, this can happen in constrained environments or with certain TLS configurations.

🔎 More robust constructor
-    pub fn new(api_key: String, default_model: Option<String>) -> Self {
+    pub fn new(api_key: String, default_model: Option<String>) -> Result<Self> {
         let client = Client::builder()
             .timeout(Duration::from_secs(60))
-            .build()
-            .expect("Failed to create HTTP client");
+            .build()?;

-        Self {
+        Ok(Self {
             client,
             api_key,
             default_model,
-        }
+        })
     }

This requires updating all call sites to handle the Result, but provides better error handling for library users.


112-132: Consider structured error types for more robust retry logic.

Line 122 uses e.to_string().contains("429") to detect rate limit errors. This string matching is fragile and may break if error message formats change.

🔎 More robust error handling

Consider defining a custom error type that preserves the status code:

#[derive(Debug)]
pub enum OpenRouterError {
    RateLimit { status: u16, message: String },
    Authentication { status: u16, message: String },
    Network(reqwest::Error),
    Parse(serde_json::Error),
    Other { status: Option<u16>, message: String },
}

Then in make_request_with_retry:

 loop {
     match self.make_request(&request).await {
         Ok(response) => return Ok(response),
         Err(e) => {
-            // Check if it's a rate limit error (429)
-            if e.to_string().contains("429") && retries > 0 {
+            if matches!(e.downcast_ref::<OpenRouterError>(), Some(OpenRouterError::RateLimit { .. })) && retries > 0 {
                 retries -= 1;
                 tokio::time::sleep(delay).await;
                 delay *= 2;
                 continue;
             }
             return Err(e);
         }
     }
 }

This makes the retry logic more explicit and maintainable.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 18d4700 and 53bce83.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (29)
  • .cargo/config.toml
  • .gitignore
  • CONTRIBUTING.md
  • Cargo.toml
  • README.md
  • benches/startup.rs
  • examples/test_context.rs
  • scripts/hooks/pre-commit
  • scripts/install-hooks.sh
  • scripts/lint.sh
  • scripts/pre-commit.sh
  • src/ai/chain.rs
  • src/ai/handler.rs
  • src/ai/providers/openrouter.rs
  • src/cli/mod.rs
  • src/color/mod.rs
  • src/config/cache.rs
  • src/config/file.rs
  • src/config/merger.rs
  • src/config/mod.rs
  • src/context/gatherer.rs
  • src/error/mod.rs
  • src/lib.rs
  • src/logging/file_logger.rs
  • src/logging/mod.rs
  • src/main.rs
  • src/output/mod.rs
  • test_config.sh
  • tests/test_context_gathering.rs
🚧 Files skipped from review as they are similar to previous changes (8)
  • examples/test_context.rs
  • src/color/mod.rs
  • src/context/gatherer.rs
  • .cargo/config.toml
  • src/config/cache.rs
  • src/lib.rs
  • .gitignore
  • tests/test_context_gathering.rs
🧰 Additional context used
🧬 Code graph analysis (8)
src/config/merger.rs (2)
src/config/loader.rs (1)
  • load_all_configs (134-173)
src/config/file.rs (6)
  • default (143-165)
  • default (170-175)
  • default (179-186)
  • default (190-195)
  • default (199-204)
  • default (208-215)
benches/startup.rs (5)
src/config/cache.rs (2)
  • get_file_config (29-45)
  • reset_config_cache (51-54)
src/context/gatherer.rs (1)
  • gather_context (41-102)
src/signals/mod.rs (1)
  • setup_signal_handlers (29-52)
src/config/mod.rs (1)
  • from_cli (32-71)
src/context/history.rs (1)
  • read_history_tail (69-105)
src/error/mod.rs (2)
src/ai/providers/openrouter.rs (2)
  • get_file_logger (22-24)
  • new (55-66)
src/logging/file_logger.rs (1)
  • new (42-66)
src/ai/handler.rs (3)
src/ai/prompt.rs (4)
  • build_chat_request (119-132)
  • build_multi_chat_request (148-182)
  • build_prompt (44-80)
  • extract_command (95-105)
src/context/gatherer.rs (1)
  • gather_context (41-102)
src/ai/chain.rs (1)
  • new (29-44)
src/logging/file_logger.rs (1)
src/logging/mod.rs (2)
  • log (117-121)
  • error (124-126)
src/ai/chain.rs (2)
src/ai/providers/openrouter.rs (6)
  • new (55-66)
  • name (283-285)
  • api_key_from_env (74-76)
  • complete (227-281)
  • response (203-203)
  • is_available (287-289)
src/config/file.rs (6)
  • default (143-165)
  • default (170-175)
  • default (179-186)
  • default (190-195)
  • default (199-204)
  • default (208-215)
src/logging/mod.rs (2)
src/color/mod.rs (1)
  • color_mode_from_config (68-80)
src/logging/file_logger.rs (1)
  • log (74-88)
src/main.rs (5)
src/cli/mod.rs (1)
  • parse_args (85-87)
src/output/mod.rs (1)
  • print_command (24-36)
src/safety/detector.rs (1)
  • is_dangerous_command (26-39)
src/signals/mod.rs (3)
  • is_interactive (81-83)
  • is_interrupted (56-58)
  • setup_signal_handlers (29-52)
src/config/mod.rs (1)
  • from_cli (32-71)
🪛 LanguageTool
README.md

[grammar] ~1-~1: Ensure spelling is correct
Context: # clai A CLI tool that converts natural languag...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🪛 markdownlint-cli2 (0.18.1)
CONTRIBUTING.md

27-27: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (24)
README.md (1)

1-67: Documentation is clear and user-friendly.

The README provides a good overview of the tool with practical examples, straightforward installation steps, and clear configuration guidance. The options table and usage examples help users understand the tool's capabilities quickly.

CONTRIBUTING.md (1)

10-17: All referenced scripts and Cargo features verified to exist. The documentation accurately reflects the available build tools and processes in the repository:

  • ./scripts/install-hooks.sh
  • ./scripts/pre-commit.sh
  • --features bench ✓ (defined in Cargo.toml)

The documented workflow is valid and complete.

Cargo.toml (2)

36-41: Release profile settings look good.

The release profile is well-configured for optimized binary size and performance: single codegen unit, LTO enabled, abort on panic, max optimization, and symbol stripping.


27-28: No action needed — dependency versions are valid.

Both tokio 1.49 and reqwest 0.13 are currently available stable releases on crates.io. The specified versions are correct and do not pose a compatibility issue.

Likely an incorrect or invalid review comment.

scripts/hooks/pre-commit (1)

12-16: LGTM!

The clippy and test steps are correctly configured with -D warnings to treat warnings as errors.

scripts/lint.sh (1)

1-15: LGTM!

The lint script correctly uses --check with cargo fmt for validation without modification, and properly treats clippy warnings as errors with -D warnings.

scripts/install-hooks.sh (1)

26-35: LGTM!

Clear and helpful user guidance about hook behavior and bypass options.

src/output/mod.rs (2)

24-36: LGTM!

The print_command function correctly handles TTY vs piped output, following UNIX conventions. Flushing stdout for piped output ensures the command is immediately available to downstream processes.


8-10: Placeholder implementation—ensure this is addressed before release.

The format_output function returns a placeholder message. This appears to be stub code that should be replaced with actual command generation logic.

test_config.sh (1)

31-31: Critical: Hardcoded absolute path breaks portability.

The script still uses hardcoded /home/vee/Coding/clAI throughout, making it non-portable. This was flagged in a previous review.

Likely an incorrect or invalid review comment.

src/cli/mod.rs (1)

82-87: LGTM!

The parse_args function is clean, pure, and properly delegates to clap's try_parse(). Good separation of concerns.

src/ai/chain.rs (1)

29-44: LGTM on constructor logic.

The constructor correctly builds the provider chain by prepending the default provider if not already in the fallback list. This ensures the default is always tried first.

src/ai/handler.rs (2)

139-173: LGTM on generate_command orchestration.

The function properly chains context building, provider creation, request construction, and response extraction with good error context using anyhow.


82-101: Refactor manual Cli reconstruction to use a helper method.

This manual Cli construction duplicates field mapping and is fragile. This concern was previously raised. Consider adding Config::to_cli(&self) -> Cli to centralize this conversion.

Likely an incorrect or invalid review comment.

src/config/merger.rs (2)

16-32: LGTM on merge_all_configs orchestration.

The function correctly implements the documented precedence order: defaults → file config → env vars → CLI flags. The error handling properly propagates ConfigLoadError.


187-192: Silent failure on invalid boolean environment variables.

Invalid boolean values (e.g., typos like "ture") silently default to false without warning. This could mask configuration errors. Same issue exists on line 203 for safety_confirm_dangerous.

Likely an incorrect or invalid review comment.

src/error/mod.rs (1)

50-67: LGTM on exit code mapping.

The exit_code() implementation correctly maps error variants to distinct exit codes per the FR-7 specification, enabling proper error differentiation by shell scripts.

src/main.rs (2)

76-136: LGTM on run_main error handling flow.

The function properly distinguishes between non-fatal config errors (NotFound → use defaults) and fatal ones (parse/permission → exit code 3). Good separation of concerns with the was_config_missing flag for verbose logging.


236-258: Reconstructing Cli from Config is verbose and fragile.

This manual reconstruction duplicates field mapping. Consider adding Config::to_cli() or refactoring should_prompt to accept Config directly.

Likely an incorrect or invalid review comment.

src/logging/mod.rs (1)

1-190: LGTM! Well-structured logging module.

The logging module is well-designed with clear separation between pure functions (formatting, level calculation) and the single side-effectful method (log). The implementation correctly uses ordering traits for log level comparison, and the tests provide good coverage.

src/logging/file_logger.rs (1)

1-278: LGTM! Robust file logger implementation.

The FileLogger is well-implemented with appropriate error handling (silent failures for robustness in debug logging), thread-safe writes, and automatic log rotation. The use of Howard Hinnant's date algorithm for timestamp generation is a solid choice.

src/config/mod.rs (1)

1-221: LGTM! Clean config implementation.

The Config struct and from_cli transformation are well-designed with proper validation (clamping), reasonable defaults, and good test coverage. The pure transformation approach is appropriate.

src/ai/providers/openrouter.rs (1)

225-290: LGTM! Well-implemented Provider trait.

The OpenRouterProvider implementation is solid with comprehensive error handling, appropriate logging, and clear model selection logic. The integration with the optional FileLogger is well done.

src/config/file.rs (1)

1-277: LGTM! Clean TOML config structure.

The FileConfig and nested structs provide a well-organized TOML configuration model with appropriate defaults, good use of serde features (flatten, rename_all), and comprehensive test coverage.

These developer-specific configurations should remain local to each
developer rather than being tracked in Git. Added .taskmaster/ and
.cursor/ to .gitignore.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
.gitignore (1)

4-10: Consider removing Node.js-specific patterns if the project is pure Rust.

The .gitignore includes patterns for Node.js artifacts (npm-debug.log*, yarn-debug.log*, yarn-error.log*, node_modules/), which appear unnecessary for a Rust CLI project. If the project does not use Node.js tooling (bundlers, documentation generators, etc.), these patterns should be removed to keep the .gitignore focused and maintainable.

If these patterns should remain (e.g., the project uses Node.js tools alongside Rust), no action is needed. Otherwise, consider applying this change:

-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-dev-debug.log
+# Logs
+logs
+*.log
+dev-debug.log

-# Dependency directories
-node_modules/
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 53bce83 and c388a13.

📒 Files selected for processing (1)
  • .gitignore
🔇 Additional comments (1)
.gitignore (1)

1-39: Repository hygiene configuration looks solid overall.

The .gitignore establishes good practices by excluding Rust build outputs (/target), environment files (.env), local configs (.clai.toml), and developer-specific directories (.taskmaster/, .cursor/). Editor and OS-specific patterns are appropriate.

Repository owner deleted a comment from coderabbitai bot Jan 7, 2026
@Vedaant-Rajoo
Copy link
Owner Author

@greptile

@greptile-apps
Copy link

greptile-apps bot commented Jan 7, 2026

Greptile found no issues!

From now on, if a review finishes and we haven't found any issues, we will not post anything, but you can confirm that we reviewed your changes in the status check section.

This feature can be toggled off in your Code Review Settings by deselecting "Create a status check for each PR".

@coderabbitai
Copy link

coderabbitai bot commented Jan 7, 2026

Walkthrough

Introduces a complete Rust CLI application "clai" for AI-powered shell command generation. Adds project infrastructure (Cargo, configuration system), AI integration with OpenRouter provider chain, context gathering from system/history/files, configuration merging from files/environment/CLI, safety checks for dangerous commands, logging system, and comprehensive tests.

Changes

Cohort / File(s) Summary
Project Infrastructure
.cargo/config.toml, .gitignore, Cargo.toml, Makefile.toml, README.md, CONTRIBUTING.md
Project manifest, build configuration, cargo aliases, development guidelines, and git ignore patterns for Rust/Node/editor artifacts.
Build & Development Scripts
scripts/hooks/pre-commit, scripts/install-hooks.sh, scripts/lint.sh, scripts/pre-commit.sh, test_config.sh, test_openrouter.sh
Pre-commit hook installation, lint/format/test automation, integration test scripts for configuration and OpenRouter API validation.
AI Integration Layer
src/ai/chain.rs, src/ai/handler.rs, src/ai/mod.rs, src/ai/prompt.rs, src/ai/provider.rs, src/ai/providers/mod.rs, src/ai/providers/openrouter.rs, src/ai/types.rs
Provider chain with multi-provider fallback, OpenRouter HTTP client with retry logic, prompt building with context injection, command extraction via regex and JSON parsing, type definitions for chat requests/responses. High logic density in provider chain initialization, request/response conversion, and error handling.
Configuration System
src/config/file.rs, src/config/loader.rs, src/config/merger.rs, src/config/cache.rs, src/config/paths.rs, src/config/mod.rs
TOML schema definitions, file permission validation (Unix 0600), environment variable resolution and merging, multi-source precedence (CLI > Env > File), caching, and path discovery. Complex merge logic across nested structures with multiple sources.
Context Gathering
src/context/system.rs, src/context/history.rs, src/context/directory.rs, src/context/stdin.rs, src/context/gatherer.rs, src/context/mod.rs
System information collection (OS, architecture, memory), shell history tail-reading, directory scanning with optional path redaction, stdin piping detection, JSON aggregation of context data. System interaction and path handling requires careful review.
CLI & User Interface
src/cli/mod.rs, src/color/mod.rs, src/output/mod.rs, src/locale/mod.rs
Clap-based CLI argument parsing with numerous options, color mode detection (TTY, environment, config), output formatting, locale/language detection.
Safety & Prompt Handling
src/safety/detector.rs, src/safety/patterns.rs, src/safety/confirmation.rs, src/safety/interactive.rs, src/safety/prompt.rs, src/safety/mod.rs
Dangerous command detection via regex patterns with caching, user confirmation prompts with stderr color output, interactive Tab-driven command selection with raw terminal mode, decision logic for execution/copy/abort flows. Interactive terminal control and signal handling require careful testing.
Core Infrastructure
src/lib.rs, src/main.rs, src/error/mod.rs, src/logging/mod.rs, src/logging/file_logger.rs, src/signals/mod.rs
Asynchronous main orchestration with signal handling, error enum with exit code mapping, file-based JSON Lines logging with truncation, TTY detection and interruption handling. Main loop error flow and logging initialization logic.
Examples & Benchmarks
examples/test_context.rs, benches/startup.rs, tests/cli_tests.rs, tests/test_context_gathering.rs
Context gathering demonstration, startup performance benchmarks via Criterion, CLI integration tests, context JSON validation tests.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Feature/rust cli implementation' is vague and uses non-descriptive terms like 'Feature' and 'implementation' that don't convey the specific nature of the changeset. Consider a more specific title that highlights the main change, such as 'Implement Rust CLI tool for AI-powered shell command generation' or 'Add clai: AI-powered CLI for shell command suggestions'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom Pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

🤖 Fix all issues with AI agents
In @benches/startup.rs:
- Around line 142-147: The benchmark repeatedly registers signal handlers by
calling setup_signal_handlers() inside the closure, causing accumulated
handlers; move the one-time call out of the iterative benchmark so handlers are
registered once (e.g., call setup_signal_handlers() before group.bench_function)
or remove this benchmark entirely; alternatively change to a setup/teardown
style (use iter_batched or a before-hook) that ensures setup_signal_handlers()
runs only once per benchmark trial rather than per iteration, and ensure any
returned SigId values from signal_hook::flag::register() are stored or
unregistered as needed.

In @Cargo.toml:
- Line 5: The authors field in Cargo.toml contains placeholder text; either
replace authors = ["Your Name <you@example.com>"] with the actual project
author(s) information (e.g., real names and contact emails) or remove the entire
authors entry if you prefer not to include it, ensuring Cargo.toml no longer
contains the placeholder string.

In @CONTRIBUTING.md:
- Around line 27-28: The fenced code block that currently contains just "src/"
is missing a language identifier and triggers markdown linting; update the
fenced block that contains "src/" to include a language tag (for example use
"text" or "bash") after the opening triple backticks so the block becomes a
properly labeled fenced code block.
- Around line 62-65: Update the CONTRIBUTING.md config precedence list to
reflect the implementation in src/config/paths.rs by adding the XDG_CONFIG_HOME
entry: include `$XDG_CONFIG_HOME/clai/config.toml` as the second priority (after
`./.clai.toml`), shifting the user and system paths accordingly so the four
locations match the code: `./.clai.toml`, `$XDG_CONFIG_HOME/clai/config.toml`,
`~/.config/clai/config.toml`, and `/etc/clai/config.toml`.

In @scripts/hooks/pre-commit:
- Around line 9-10: The pre-commit hook currently runs "cargo fmt" which can
modify files but those edits won't be committed; update the hook to run "cargo
fmt -- --check" so the hook fails when formatting is required instead of
mutating files during commit. Modify the command in the pre-commit script (the
block containing the echo "1. Formatting code..." and the cargo fmt invocation)
to use the check flag, ensuring formatting is enforced without changing the
working tree.

In @src/color/mod.rs:
- Around line 157-172: The test test_detect_color_auto_clicolor mutates the
global CLICOLOR env var causing race conditions; make it isolated by either (A)
adding serial_test as a dev-dependency and annotating the test
(test_detect_color_auto_clicolor) with the serial attribute so it runs serially,
or (B) using a scoped env helper like the temp-env crate to set CLICOLOR only
for the duration of the test (wrap the call to detect_color_auto in a scoped
with_var/guard) and avoid calling std::env::set_var/remove_var directly; update
Cargo.toml dev-dependencies and the test accordingly.

In @src/config/cache.rs:
- Around line 61-127: Tests always call get_file_config with identical Cli
values so they don't catch that the cache ignores CLI differences; add a new
test (e.g., test_cache_respects_different_cli_args) that calls
reset_config_cache(), then calls get_file_config with two different Cli
instances (differing at least in provider and/or model) and asserts the
resulting configs differ (use assert_ne! on provider/model fields or whole
config). Also update existing cache logic if needed so get_file_config/cache key
takes the incoming Cli (or its relevant fields) into account rather than
returning a single global cached config when different CLI options are supplied;
reference get_file_config and reset_config_cache to locate where to change
behavior and to place the new test.

In @src/config/file.rs:
- Around line 121-130: The default dangerous patterns in
default_dangerous_patterns() currently return simple substrings and conflict
with the richer regex defaults in safety::patterns (e.g., the patterns module's
regexes like r"rm\s+-rf\s+/" and r"dd\s+if=/dev/zero"); update
default_dangerous_patterns() to return the same regex pattern list used in
safety::patterns.rs (or delegate to a shared constant) so that config
deserialization uses the exact same regex defaults, ensuring both modules
reference the same identifiers (default_dangerous_patterns and the patterns
module/shared constant) and removing duplication.

In @src/context/stdin.rs:
- Around line 36-58: The current use of stdin.read(&mut buffer) can return fewer
bytes than available and truncate piped input; replace the single read call with
reading to end using a Take wrapper to cap at max_bytes: obtain a locked stdin
via io::stdin().lock(), call .take(max_bytes as u64) and use read_to_end(&mut
buffer) to fill the buffer reliably, preserve the existing UTF-8 handling via
String::from_utf8_lossy(&buffer), and propagate Err handling similarly (i.e.,
replace the match on stdin.read with a match on take.read_to_end).

In @src/main.rs:
- Around line 241-263: The code manually reconstructs a clai::cli::Cli inside
the should_prompt call, which is verbose and fragile; add a conversion method on
your Config (e.g., impl Config { fn to_cli(&self) -> clai::cli::Cli }) that maps
Config fields to a Cli and then call should_prompt(&config.to_cli(),
&file_config) or alternatively change should_prompt to accept &Config directly
(update its signature and callers) so you avoid field-by-field duplication;
ensure you reference the existing should_prompt function and
clai::cli::Cli/Config types when implementing the change.

In @src/safety/detector.rs:
- Around line 79-96: get_matching_pattern currently looks up the display pattern
by indexing config.safety.dangerous_patterns with the regex index, which can be
empty when compile_dangerous_regexes used default_dangerous_patterns; change the
fallback to use the matched regex's pattern string (regex.as_str()) instead of
the generic "pattern_{}" so the real pattern is preserved. Locate
get_matching_pattern and adjust the construction of the pattern variable
(currently using
config.safety.dangerous_patterns.get(index).cloned().unwrap_or_else(...)) to
prefer the configured pattern, but when that is None use regexes[index].as_str()
(or the loop variable `regex`) for a meaningful fallback; ensure types remain
String.

In @src/safety/patterns.rs:
- Around line 56-70: The code in the dangerous patterns compilation block logs a
warning but then immediately returns an error (in the Err(e) arm), which
contradicts the "continue with other patterns" intent; change the error handling
in that match arm (the Err(e) branch) to log the error and continue processing
remaining patterns instead of returning—i.e., remove the return
Err(...)/.context(...) and replace it with a continue (or otherwise skip this
pattern), keeping the eprintln! call as-is so invalid regexes are warned but do
not abort compilation of other patterns in the function that builds/iterates
dangerous_patterns.

In @test_config.sh:
- Line 110: The command substitution assigned to OUTPUT uses a duplicated stderr
redirect; update the assignment (the OUTPUT=$(cargo r -- "test" 2>&1 2>&1)
expression) to remove the redundant `2>&1` so only a single `2>&1` remains,
preserving the intended stdout+stderr capture.
- Around line 22-30: The test_result function reads $? inside itself so it sees
the status of the if statement, not the prior test; change the API so
test_result accepts an explicit exit status (e.g., first parameter) and use that
to increment TESTS_PASSED/TESTS_FAILED and print the message (second parameter),
then update all call sites (e.g., the invocations around the existing test
calls) to pass "$?" as the first arg and the human-readable test name as the
second arg for every test invocation.
🧹 Nitpick comments (37)
.gitignore (1)

4-6: Remove Node.js-specific patterns from a Rust-only project.

This .gitignore includes Node.js dependency and logging patterns (npm-debug.log*, yarn-debug.log*, yarn-error.log*, node_modules/) that are unnecessary for a Rust CLI project. These likely came from a template and add noise.

🔎 Proposed cleanup
 # Logs
 logs
 *.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
 dev-debug.log

-# Dependency directories
-node_modules/

Also applies to: 10-10

scripts/pre-commit.sh (1)

1-18: Ensure the script is executable and consider expanding clippy coverage.

The script looks good overall with proper error handling and clear user feedback.

Consider these improvements:

  1. Verify the script has executable permissions (chmod +x scripts/pre-commit.sh).
  2. Add --all-targets to clippy to check tests, examples, and benchmarks:
🔎 Suggested improvement
 echo "2. Running clippy..."
-cargo clippy -- -D warnings
+cargo clippy --all-targets -- -D warnings
.cargo/config.toml (1)

1-32: LGTM! The cargo aliases are well-structured.

The aliases correctly avoid shell operators and provide helpful documentation about the limitation. The reference to scripts/pre-commit.sh for combined workflows is a good solution.

Optional enhancement: Consider adding a ca alias for cargo add (if using Cargo 1.62+) to complement the existing build/test/run aliases.

🔎 Optional addition
 # Clean
 clean-all = "clean"
+ca = "add"
Makefile.toml (2)

31-34: Consider adding --all-targets to clippy for consistency.

To ensure comprehensive linting coverage across all code (including tests, examples, and benchmarks), consider adding the --all-targets flag:

🔎 Suggested improvement
 [tasks.clippy]
 description = "Run clippy linter"
 command = "cargo"
-args = ["clippy", "--", "-D", "warnings"]
+args = ["clippy", "--all-targets", "--", "-D", "warnings"]

69-72: Document cross-compilation target choices.

The cross-build task hardcodes specific Linux musl targets (x86_64 and aarch64). Consider documenting why these targets were chosen or making them configurable through environment variables if other targets might be needed.

test_openrouter.sh (1)

1-72: Good test coverage, but consider performance optimization.

The script provides comprehensive testing of OpenRouter integration with good user feedback and error handling.

Suggestions for improvement:

  1. Ensure executable permissions: Verify the script has execute permissions (chmod +x test_openrouter.sh).

  2. Performance: Using cargo run repeatedly is slow as it checks for recompilation each time. Consider building once and running the binary:

🔎 Performance optimization
+echo "Building clai..."
+cargo build --quiet
+CLAI_BIN="./target/debug/clai"
+
 echo "1. Testing basic command generation..."
 echo "   Command: 'list files in current directory'"
 echo ""
 
-COMMAND=$(cargo run --quiet -- "list files in current directory" 2>&1)
+COMMAND=$($CLAI_BIN "list files in current directory" 2>&1)
 EXIT_CODE=$?

Apply similar changes to the other cargo run invocations.

scripts/lint.sh (1)

1-15: Optional: Consider enhanced error reporting.

The script works correctly but could provide better developer experience by reporting which specific check failed and running all checks even if one fails (collecting results at the end).

🔎 Proposed enhancement
 #!/usr/bin/env bash
 # Linting checks for clai
 # Runs clippy and format checks without making changes
 
-set -e
+FAILED=0
 
 echo "Running lint checks..."
 
 echo "1. Checking code formatting..."
-cargo fmt -- --check
+if ! cargo fmt -- --check; then
+    echo "   ✗ Format check failed"
+    FAILED=1
+fi
 
 echo "2. Running clippy..."
-cargo clippy -- -D warnings
+if ! cargo clippy -- -D warnings; then
+    echo "   ✗ Clippy check failed"
+    FAILED=1
+fi
 
-echo "✓ All lint checks passed!"
+if [ $FAILED -eq 0 ]; then
+    echo "✓ All lint checks passed!"
+    exit 0
+else
+    echo "✗ Some lint checks failed"
+    exit 1
+fi

This allows developers to see all failures at once rather than fixing them one at a time.

scripts/install-hooks.sh (1)

17-24: Consider verifying the hook installation succeeded.

The script reports success even if the copy operation fails silently. While the current structure with set -e should exit on copy failure, adding an explicit verification would make the success message more reliable.

🔎 Proposed enhancement
 # Install pre-commit hook
 if [ -f "$SCRIPTS_HOOKS_DIR/pre-commit" ]; then
     echo "Installing pre-commit hook..."
     cp "$SCRIPTS_HOOKS_DIR/pre-commit" "$HOOKS_DIR/pre-commit"
     chmod +x "$HOOKS_DIR/pre-commit"
-    echo "✓ Pre-commit hook installed"
+    if [ -x "$HOOKS_DIR/pre-commit" ]; then
+        echo "✓ Pre-commit hook installed"
+    else
+        echo "✗ Error: Failed to install pre-commit hook"
+        exit 1
+    fi
 else
     echo "✗ Warning: scripts/hooks/pre-commit not found"
 fi
examples/test_context.rs (2)

65-65: Consider using !files.is_empty() for idiomatic Rust.

While functionally equivalent, !files.is_empty() is the idiomatic way to check for non-empty collections in Rust.

🔎 Proposed fix
-                    if files.len() > 0 {
+                    if !files.is_empty() {

90-92: String slicing may panic on multibyte UTF-8 boundaries.

The preview logic slices at byte index 50 without checking char boundaries. For multibyte UTF-8 characters, this could panic.

🔎 Proposed fix
                             let preview = if s.len() > 50 {
-                                format!("{}...", &s[..50])
+                                let mut end = 50;
+                                while end > 0 && !s.is_char_boundary(end) {
+                                    end -= 1;
+                                }
+                                format!("{}...", &s[..end])
                             } else {

Alternatively, use chars().take(50):

                             let preview = if s.chars().count() > 50 {
-                                format!("{}...", &s[..50])
+                                format!("{}...", s.chars().take(50).collect::<String>())
                             } else {
tests/test_context_gathering.rs (1)

50-56: Improve panic messages for type assertions.

The unwrap() calls on as_object() will panic with a generic message if the value isn't an object. Using expect() with descriptive messages would make test failures clearer.

🔎 Suggested improvement
-        let system = parsed.get("system").unwrap().as_object().unwrap();
+        let system = parsed
+            .get("system")
+            .expect("system field should exist")
+            .as_object()
+            .expect("system should be an object");

Apply similar changes to lines 60, 66, and 72 for cwd, files, and history assertions.

src/safety/patterns.rs (1)

98-108: reset_regex_cache() is a no-op and misleading.

The function claims to reset the cache for testing but does nothing because OnceLock doesn't support reset. The comment acknowledges this but the function's existence is misleading. Either remove the function or document clearly that tests should use compile_dangerous_regexes directly.

🔎 Proposed fix: Remove misleading function or clarify API
-/// Reset dangerous regex cache (for testing only)
-///
-/// # Safety
-/// This function is only intended for testing.
-/// It clears the cache, allowing tests to use different configs.
-#[cfg(test)]
-pub fn reset_regex_cache() {
-    // OnceLock doesn't have a reset method, so we can't actually reset it
-    // This is a no-op, but documents the intent for testing
-    // In practice, tests should use different configs or test compile_dangerous_regexes directly
-}

Tests should call compile_dangerous_regexes directly instead of relying on get_dangerous_regexes.

src/config/paths.rs (2)

14-38: System-wide config path is Unix-specific.

The hardcoded /etc/clai/config.toml path (line 35) won't exist or work correctly on Windows. Consider using a platform-specific system config path or making this conditional.

🔎 Proposed platform-aware fix
     // 4. System-wide config (lowest priority)
-    paths.push(PathBuf::from("/etc/clai/config.toml"));
+    #[cfg(unix)]
+    paths.push(PathBuf::from("/etc/clai/config.toml"));
+    
+    #[cfg(windows)]
+    if let Some(program_data) = std::env::var_os("ProgramData") {
+        paths.push(PathBuf::from(program_data).join("clai").join("config.toml"));
+    }

115-152: Test isolation issue with environment variable mutations.

Tests test_get_xdg_config_path_with_env and test_get_xdg_config_path_without_env modify XDG_CONFIG_HOME without proper isolation. While they attempt to restore the original value, this can still cause race conditions with parallel test execution. Consider using serial_test or temp-env crate.

src/ai/providers/openrouter.rs (2)

55-66: Consider returning Result from new() instead of panicking.

The expect("Failed to create HTTP client") on line 59 will panic if client creation fails. While this is unlikely, it would be more Rust-idiomatic to return a Result for fallible constructors.

🔎 Proposed change
-    pub fn new(api_key: String, default_model: Option<String>) -> Self {
+    pub fn new(api_key: String, default_model: Option<String>) -> Result<Self> {
         let client = Client::builder()
             .timeout(Duration::from_secs(60))
-            .build()
-            .expect("Failed to create HTTP client");
+            .build()?;
 
-        Self {
+        Ok(Self {
             client,
             api_key,
             default_model,
-        }
+        })
     }

120-128: Brittle rate limit detection via string matching.

The check e.to_string().contains("429") relies on the error message containing "429". This is fragile as error message formats can change. Consider checking the HTTP status code directly when available.

🔎 Proposed improvement

Instead of string matching, consider propagating a typed error from make_request that includes the status code:

enum ApiError {
    RateLimit(String),
    Network(String),
    Other(anyhow::Error),
}

Then match on the error variant for retry decisions.

src/config/merger.rs (1)

276-293: Test modifies global environment state.

Setting and removing environment variables in tests can cause flaky behavior when tests run in parallel (Rust's default). Consider using serial_test crate or #[serial] attribute for tests that modify global state, or use temp_env crate for scoped environment variables.

src/safety/confirmation.rs (1)

137-149: Consider simplifying the character extraction chain.

The double unwrap() calls on lines 140 and 143 are safe (guarded by the is_empty() check), but the pattern is verbose. Consider using to_ascii_uppercase() directly on the char.

🔎 Proposed simplification
-                match trimmed
-                    .chars()
-                    .next()
-                    .unwrap()
-                    .to_uppercase()
-                    .next()
-                    .unwrap()
-                {
+                match trimmed.chars().next().unwrap().to_ascii_uppercase() {
                     'E' => Ok(Decision::Execute),
                     'C' => Ok(Decision::Copy),
                     'A' => Ok(Decision::Abort),
                     _ => Err(ConfirmationError::InvalidInput(input.trim().to_string())),
                 }
src/ai/handler.rs (1)

147-161: Consider extracting duplicate debug output logic.

The debug output code (printing model, temperature, max_tokens, messages) is duplicated between generate_command and generate_commands. Consider extracting to a helper.

🔎 Proposed helper
fn debug_print_request(request: &ChatRequest, extra_info: Option<&str>) {
    eprintln!("\n=== DEBUG: Request to be sent to AI ===");
    eprintln!("Model: {:?}", request.model);
    eprintln!("Temperature: {:?}", request.temperature);
    eprintln!("Max Tokens: {:?}", request.max_tokens);
    if let Some(info) = extra_info {
        eprintln!("{}", info);
    }
    eprintln!("\nMessages:");
    for (i, msg) in request.messages.iter().enumerate() {
        eprintln!("  {}. Role: {:?}", i + 1, msg.role);
        eprintln!("     Content: {}", msg.content);
        if i < request.messages.len() - 1 {
            eprintln!();
        }
    }
    eprintln!("=== END DEBUG ===\n");
}

Also applies to: 201-216

src/context/system.rs (1)

32-85: Consider handling RwLock poisoning gracefully.

The unwrap() calls on SYSTEM_INFO_CACHE.read() (line 35) and SYSTEM_INFO_CACHE.write() (line 80) will panic if the lock becomes poisoned (i.e., a thread panicked while holding the lock). While this is rare in practice, for a CLI tool it may be worth handling gracefully.

🔎 Proposed fix using unwrap_or_else for read
     // Check cache
     {
-        let cache = SYSTEM_INFO_CACHE.read().unwrap();
+        let cache = SYSTEM_INFO_CACHE.read().unwrap_or_else(|poisoned| poisoned.into_inner());
         if let Some(ref info) = *cache {
             return info.clone();
         }
     }

This pattern recovers from a poisoned lock by extracting the inner value, which is often acceptable for read-only cached data.

src/context/directory.rs (1)

180-207: Test modifies global state (current directory) which may cause flaky tests.

The test changes std::env::current_dir() which is global process state. If tests run in parallel, this could cause race conditions or unexpected behavior in other tests.

Consider using #[serial] from the serial_test crate or restructuring scan_directory to accept a path parameter for testing purposes:

// Alternative: Add an internal function that accepts a path
fn scan_directory_at(dir: &Path, max_files: u32, redact_paths: bool) -> Vec<String> { ... }

pub fn scan_directory(max_files: u32, redact_paths: bool) -> Vec<String> {
    match std::env::current_dir() {
        Ok(path) => scan_directory_at(&path, max_files, redact_paths),
        Err(_) => Vec::new(),
    }
}
src/ai/chain.rs (1)

96-119: Mutex lock may panic if poisoned.

The unwrap() on self.provider_instances.lock() (line 97) will panic if the Mutex becomes poisoned. Consider using unwrap_or_else(|e| e.into_inner()) for graceful recovery.

🔎 Proposed fix
     fn get_provider(&self, index: usize) -> Result<Arc<dyn Provider>> {
-        let mut instances = self.provider_instances.lock().unwrap();
+        let mut instances = self.provider_instances.lock().unwrap_or_else(|e| e.into_inner());
src/ai/types.rs (1)

146-149: Consider validating temperature range.

The doc comment mentions temperature should be 0.0 to 2.0, but no validation is performed. Invalid values could cause API errors at runtime.

🔎 Proposed validation
     pub fn with_temperature(mut self, temperature: f64) -> Self {
+        // Clamp to valid range (0.0 to 2.0 for most providers)
+        let temperature = temperature.clamp(0.0, 2.0);
         self.temperature = Some(temperature);
         self
     }

Alternatively, return a Result or document that validation is the caller's responsibility.

src/safety/interactive.rs (1)

171-186: Consider logging terminal command failures instead of silently ignoring.

The let _ = stderr.execute(...) calls silently discard errors from terminal manipulation commands. While these failures are often non-critical, logging them in debug mode could help diagnose display issues.

🔎 Proposed improvement
-                        let _ = stderr.execute(MoveUp(1));
-                        let _ = stderr.execute(MoveToColumn(0));
-                        let _ = stderr.execute(Clear(ClearType::CurrentLine));
+                        if let Err(e) = stderr.execute(MoveUp(1)) {
+                            // Terminal manipulation failed, continue anyway
+                            eprintln!("\r"); // Best-effort fallback
+                        }
+                        let _ = stderr.execute(MoveToColumn(0));
+                        let _ = stderr.execute(Clear(ClearType::CurrentLine));

Alternatively, this is acceptable as-is since terminal display issues are non-critical for functionality.

src/context/history.rs (2)

69-101: Potential issue: First line may be partial when seeking mid-file.

When seeking 4096 bytes from the end, the first line read is likely a partial line (landing in the middle of a command). The current implementation includes this partial line in the result, which could confuse users or downstream consumers.

Consider discarding the first line when seeking to a non-zero position:

🔎 Proposed fix
     // Seek to position for tail reading (4096 bytes from end, or start if smaller)
     let seek_pos = file_size.saturating_sub(4096);

     if reader.seek(SeekFrom::Start(seek_pos)).is_err() {
         return Vec::new();
     }

-    // Read all lines from seek position
-    let lines: Vec<String> = reader.lines().map_while(Result::ok).collect();
+    // Read all lines from seek position
+    let mut lines: Vec<String> = reader.lines().map_while(Result::ok).collect();
+
+    // If we seeked to middle of file, discard first (likely partial) line
+    if seek_pos > 0 && !lines.is_empty() {
+        lines.remove(0);
+    }

191-213: Test for large file may not trigger the partial-line scenario.

The test creates 20 lines with ~60 chars each (~1.2 KB total), which is smaller than the 4096-byte seek threshold. To properly test the tail behavior with seeking, the file should exceed 4096 bytes.

🔎 Proposed fix
 #[test]
 fn test_read_history_tail_large_file() {
-    // Create temp file with 20 lines (larger than 4096 bytes when written)
+    // Create temp file with enough lines to exceed 4096 bytes
     let mut temp_file = NamedTempFile::new().unwrap();
-    for i in 1..=20 {
-        writeln!(
-            temp_file,
-            "command_{}_with_some_additional_text_to_make_line_longer",
-            i
-        )
-        .unwrap();
+    // ~100 chars per line * 50 lines = ~5KB (exceeds 4096 threshold)
+    for i in 1..=50 {
+        writeln!(temp_file, "command_{:03}_with_padding_to_ensure_line_is_long_enough_to_exceed_threshold_{}", i, "x".repeat(50)).unwrap();
     }
     temp_file.flush().unwrap();
src/logging/mod.rs (1)

48-69: Consider reducing duplication between color and non-color branches.

Both branches in format_log repeat the level-to-prefix mapping. A small refactor could reduce this:

🔎 Proposed fix
 pub fn format_log(level: LogLevel, message: &str, color_mode: ColorMode) -> String {
     let use_color = color_mode.should_use_color();
+    let (prefix, color) = match level {
+        LogLevel::Error => ("ERROR", "red"),
+        LogLevel::Warning => ("WARN", "yellow"),
+        LogLevel::Info => ("INFO", "blue"),
+        LogLevel::Debug => ("DEBUG", "cyan"),
+        LogLevel::Trace => ("TRACE", "magenta"),
+    };

     if use_color {
-        match level {
-            LogLevel::Error => format!("{} {}", colorize("ERROR", "red"), message),
-            LogLevel::Warning => format!("{} {}", colorize("WARN", "yellow"), message),
-            LogLevel::Info => format!("{} {}", colorize("INFO", "blue"), message),
-            LogLevel::Debug => format!("{} {}", colorize("DEBUG", "cyan"), message),
-            LogLevel::Trace => format!("{} {}", colorize("TRACE", "magenta"), message),
-        }
+        format!("{} {}", colorize(prefix, color), message)
     } else {
-        // No color - just prefix with level
-        match level {
-            LogLevel::Error => format!("ERROR: {}", message),
-            LogLevel::Warning => format!("WARN: {}", message),
-            LogLevel::Info => format!("INFO: {}", message),
-            LogLevel::Debug => format!("DEBUG: {}", message),
-            LogLevel::Trace => format!("TRACE: {}", message),
-        }
+        format!("{}: {}", prefix, message)
     }
 }
src/signals/mod.rs (1)

33-44: Consider graceful handling instead of expect() on signal registration.

Using expect() will panic if signal registration fails (e.g., on unsupported platforms or when signal limits are reached). While signal registration failure is rare, panicking during initialization could leave the application in an undefined state.

Consider returning a Result or logging a warning and continuing without signal handling:

🔎 Proposed fix
-pub fn setup_signal_handlers() -> Arc<AtomicBool> {
+pub fn setup_signal_handlers() -> Arc<AtomicBool> {
     let interrupted = Arc::new(AtomicBool::new(false));

     // Handle SIGINT (Ctrl+C) - exit with code 130
     {
         let flag = Arc::clone(&interrupted);
-        signal_hook::flag::register(signal_hook::consts::SIGINT, flag.clone())
-            .expect("Failed to register SIGINT handler");
+        if let Err(e) = signal_hook::flag::register(signal_hook::consts::SIGINT, flag.clone()) {
+            eprintln!("Warning: Could not register SIGINT handler: {}", e);
+        }
     }

     // Handle SIGTERM - clean shutdown
     {
         let flag = Arc::clone(&interrupted);
-        signal_hook::flag::register(signal_hook::consts::SIGTERM, flag.clone())
-            .expect("Failed to register SIGTERM handler");
+        if let Err(e) = signal_hook::flag::register(signal_hook::consts::SIGTERM, flag.clone()) {
+            eprintln!("Warning: Could not register SIGTERM handler: {}", e);
+        }
     }
src/main.rs (1)

52-58: Parsing args twice may show inconsistent error messages.

If the initial parse_args() in run_main fails, the error is handled and printed. Then in the error branch (lines 52-58), parse_args() is called again just to get verbosity. If the first parse failed, this second parse will also fail, returning unwrap_or(0).

While this works, it's slightly wasteful and could be confusing. Consider storing the verbosity from the successful parse path or defaulting to 0 when parsing initially fails.

🔎 Proposed fix
         Err(err) => {
             // Log error to file if file logging is enabled
             err.log_to_file();

-            // Get verbosity level from parsed CLI args
-            // Parse args again just to get verbosity (lightweight operation)
-            let verbose = parse_args().map(|cli| cli.verbose).unwrap_or(0);
+            // Use default verbosity (0) for errors during arg parsing
+            // For other errors, we already have config but can't easily access it here
+            let verbose = 0;

             // Print error to stderr with optional backtrace
             err.print_stderr(verbose);

Alternatively, refactor to pass verbosity through the error or store it earlier.

src/error/mod.rs (1)

127-154: The backtrace() method doesn't return an actual backtrace.

The method is named backtrace() but actually returns an error chain (walking through .source()). This could be misleading. Consider renaming to error_chain() or clarifying in the documentation.

Also, the check on line 146 (if !backtrace_str.is_empty()) will always be true since line 140 always pushes content.

🔎 Proposed fix
-    /// Get backtrace if available
+    /// Get error chain if available
     ///
-    /// Extracts backtrace from anyhow error chain
-    fn backtrace(&self) -> Option<String> {
+    /// Walks the source chain to build a causal trace
+    fn error_chain(&self) -> Option<String> {
         match self {
             ClaiError::General(err)
             | ClaiError::Config { source: err }
             | ClaiError::API { source: err, .. } => {
-                // Try to get backtrace from anyhow error
-                let mut backtrace_str = String::new();
+                let mut chain = String::new();
                 let mut current: &dyn std::error::Error = err.as_ref();

                 // Build error chain
-                backtrace_str.push_str(&format!("Error: {}\n", current));
+                chain.push_str(&format!("Error: {}\n", current));
                 while let Some(source) = current.source() {
-                    backtrace_str.push_str(&format!("Caused by: {}\n", source));
+                    chain.push_str(&format!("Caused by: {}\n", source));
                     current = source;
                 }

-                if !backtrace_str.is_empty() {
-                    Some(backtrace_str)
-                } else {
-                    None
-                }
+                Some(chain)
             }
             _ => None,
         }
     }

And update the call site in print_stderr:

         // Show backtrace if verbose >= 1
         if verbose >= 1 {
-            if let Some(backtrace) = self.backtrace() {
-                eprintln!("\nBacktrace:\n{}", backtrace);
+            if let Some(chain) = self.error_chain() {
+                eprintln!("\nError chain:\n{}", chain);
             }
         }
src/config/mod.rs (1)

83-91: Path expansion only handles ~/ prefix, not standalone ~.

The expand_path function handles ~/some/path but not a standalone ~ (which should expand to the home directory itself). While unlikely for a debug log file, it's a minor edge case.

🔎 Proposed fix
     fn expand_path(path: &str) -> PathBuf {
+        if path == "~" {
+            if let Some(base_dirs) = directories::BaseDirs::new() {
+                return base_dirs.home_dir().to_path_buf();
+            }
+        }
         if let Some(stripped) = path.strip_prefix("~/") {
             if let Some(base_dirs) = directories::BaseDirs::new() {
                 return base_dirs.home_dir().join(stripped);
             }
         }
         PathBuf::from(path)
     }
src/logging/file_logger.rs (2)

82-87: Errors during logging are silently ignored.

The log() method silently ignores serialization errors (if let Ok(json)) and lock acquisition failures (if let Ok(mut guard)). While this prevents logging failures from crashing the application, it could make debugging difficult if logs silently stop appearing.

Consider logging a warning to stderr on failure, at least in debug builds:

🔎 Proposed fix
     pub fn log(&self, level: LogLevel, event: &str, data: serde_json::Value) {
         let entry = LogEntry {
             ts: iso8601_timestamp(),
             level: level_str(level),
             event,
             data,
         };

-        if let Ok(json) = serde_json::to_string(&entry) {
-            if let Ok(mut guard) = self.writer.lock() {
-                let _ = writeln!(guard, "{}", json);
-                let _ = guard.flush();
-            }
-        }
+        match serde_json::to_string(&entry) {
+            Ok(json) => match self.writer.lock() {
+                Ok(mut guard) => {
+                    let _ = writeln!(guard, "{}", json);
+                    let _ = guard.flush();
+                }
+                Err(_) => {
+                    #[cfg(debug_assertions)]
+                    eprintln!("FileLogger: Failed to acquire lock");
+                }
+            },
+            Err(_) => {
+                #[cfg(debug_assertions)]
+                eprintln!("FileLogger: Failed to serialize log entry");
+            }
+        }
     }

90-118: Consider avoiding Debug format for role serialization.

Line 102 uses format!("{:?}", m.role).to_lowercase() to serialize the role. This depends on the Debug implementation, which could change. Consider implementing a proper serialization method or using serde's rename attribute on the enum.

🔎 Proposed fix
             .map(|m| {
                 serde_json::json!({
-                    "role": format!("{:?}", m.role).to_lowercase(),
+                    "role": m.role.as_str(), // Assuming ChatRole implements as_str()
                     "content": m.content
                 })
             })

If ChatRole doesn't have an as_str() method, consider adding one or using a match expression.

src/config/loader.rs (2)

155-169: Redundant NotFound handling.

Since existing_config_paths() already filters to only return paths where config_file_exists(path) is true (see src/config/paths.rs:73-78), the NotFound branch at line 159 should never be reached. This could mask unexpected issues if the file disappears between the existence check and the load attempt.

Consider simplifying to treat all errors uniformly or logging a warning for the unexpected case.

🔎 Suggested simplification
             Err(e) => {
-                // For non-fatal errors (file not found), continue to next file
-                // For fatal errors (parse, permissions), return immediately
-                match e {
-                    ConfigLoadError::NotFound(_) => {
-                        // File not found is non-fatal - continue to next config file
-                        continue;
-                    }
-                    _ => {
-                        // Parse errors, permission errors, etc. are fatal
-                        return Err(e);
-                    }
-                }
+                // All errors from existing files are fatal (parse, permissions, etc.)
+                // NotFound is unexpected here since existing_config_paths() pre-filters
+                return Err(e);
             }

290-312: Test modifies global environment, risking flakiness in parallel runs.

std::env::set_var and remove_var mutate global process state. If other tests run concurrently and use TEST_API_KEY or rely on environment isolation, this could cause intermittent failures.

Consider using a unique, unlikely-to-collide variable name (e.g., CLAI_TEST_LOADER_API_KEY_12345) or adding #[serial] from the serial_test crate if test isolation becomes an issue.

src/config/file.rs (2)

142-166: Consider using nested Default implementations to reduce duplication.

FileConfig::default() manually constructs each nested struct, duplicating logic already in ProviderConfig::default(), ContextConfig::default(), etc.

🔎 Suggested simplification
 impl Default for FileConfig {
     fn default() -> Self {
         Self {
-            provider: ProviderConfig {
-                default: default_provider_default(),
-                fallback: Vec::new(),
-            },
-            context: ContextConfig {
-                max_files: default_max_files(),
-                max_history: default_max_history(),
-                redact_paths: false,
-                redact_username: false,
-            },
-            safety: SafetyConfig {
-                dangerous_patterns: default_dangerous_patterns(),
-                confirm_dangerous: default_confirm_dangerous(),
-            },
-            ui: UiConfig {
-                color: default_color(),
-                debug_log_file: None,
-            },
+            provider: ProviderConfig::default(),
+            context: ContextConfig::default(),
+            safety: SafetyConfig::default(),
+            ui: UiConfig::default(),
             providers: HashMap::new(),
         }
     }
 }

223-243: Simplify serialization test with direct equality check.

Since FileConfig derives PartialEq, the roundtrip test can use a single assertion.

🔎 Suggested simplification
     #[test]
     fn test_config_serialize_deserialize() {
         let config = FileConfig::default();
 
         // Serialize to TOML
         let toml_string = toml::to_string(&config).expect("Failed to serialize config");
 
         // Deserialize back
         let deserialized: FileConfig =
             toml::from_str(&toml_string).expect("Failed to deserialize config");
 
-        // Verify values match
-        assert_eq!(config.provider.default, deserialized.provider.default);
-        assert_eq!(config.context.max_files, deserialized.context.max_files);
-        assert_eq!(config.context.max_history, deserialized.context.max_history);
-        assert_eq!(
-            config.safety.dangerous_patterns,
-            deserialized.safety.dangerous_patterns
-        );
-        assert_eq!(config.ui.color, deserialized.ui.color);
+        // Verify complete equality
+        assert_eq!(config, deserialized);
     }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 291869d and 2c523a4.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (52)
  • .cargo/config.toml
  • .gitignore
  • CONTRIBUTING.md
  • Cargo.toml
  • Makefile.toml
  • README.md
  • benches/startup.rs
  • examples/test_context.rs
  • scripts/hooks/pre-commit
  • scripts/install-hooks.sh
  • scripts/lint.sh
  • scripts/pre-commit.sh
  • src/ai/chain.rs
  • src/ai/handler.rs
  • src/ai/mod.rs
  • src/ai/prompt.rs
  • src/ai/provider.rs
  • src/ai/providers/mod.rs
  • src/ai/providers/openrouter.rs
  • src/ai/types.rs
  • src/cli/mod.rs
  • src/color/mod.rs
  • src/config/cache.rs
  • src/config/file.rs
  • src/config/loader.rs
  • src/config/merger.rs
  • src/config/mod.rs
  • src/config/paths.rs
  • src/context/directory.rs
  • src/context/gatherer.rs
  • src/context/history.rs
  • src/context/mod.rs
  • src/context/stdin.rs
  • src/context/system.rs
  • src/error/mod.rs
  • src/lib.rs
  • src/locale/mod.rs
  • src/logging/file_logger.rs
  • src/logging/mod.rs
  • src/main.rs
  • src/output/mod.rs
  • src/safety/confirmation.rs
  • src/safety/detector.rs
  • src/safety/interactive.rs
  • src/safety/mod.rs
  • src/safety/patterns.rs
  • src/safety/prompt.rs
  • src/signals/mod.rs
  • test_config.sh
  • test_openrouter.sh
  • tests/cli_tests.rs
  • tests/test_context_gathering.rs
🧰 Additional context used
🧬 Code graph analysis (25)
tests/test_context_gathering.rs (2)
src/context/gatherer.rs (1)
  • gather_context (41-102)
examples/test_context.rs (1)
  • serde_json (38-38)
src/safety/patterns.rs (1)
src/config/file.rs (6)
  • default_dangerous_patterns (121-130)
  • default (143-165)
  • default (170-175)
  • default (179-186)
  • default (190-195)
  • default (199-204)
src/config/cache.rs (1)
src/config/merger.rs (1)
  • merge_all_configs (16-32)
src/output/mod.rs (1)
src/signals/mod.rs (2)
  • is_stdout_tty (62-64)
  • is_piped (88-90)
src/ai/mod.rs (3)
src/ai/chain.rs (1)
  • providers (145-147)
src/ai/handler.rs (2)
  • generate_command (139-173)
  • generate_commands (193-234)
src/ai/prompt.rs (5)
  • build_chat_request (119-132)
  • build_multi_chat_request (148-182)
  • build_prompt (44-80)
  • extract_command (95-105)
  • extract_commands (196-277)
src/ai/chain.rs (2)
src/ai/providers/openrouter.rs (6)
  • new (55-66)
  • name (283-285)
  • api_key_from_env (74-76)
  • complete (227-281)
  • response (203-203)
  • is_available (287-289)
src/config/file.rs (5)
  • default (143-165)
  • default (170-175)
  • default (179-186)
  • default (190-195)
  • default (199-204)
src/safety/detector.rs (2)
src/safety/patterns.rs (2)
  • get_dangerous_regexes (87-96)
  • compile_dangerous_regexes (42-75)
src/config/file.rs (5)
  • default (143-165)
  • default (170-175)
  • default (179-186)
  • default (190-195)
  • default (199-204)
src/logging/file_logger.rs (1)
src/logging/mod.rs (2)
  • log (117-121)
  • error (124-126)
src/safety/confirmation.rs (1)
src/signals/mod.rs (1)
  • is_stderr_tty (74-76)
tests/cli_tests.rs (1)
src/error/mod.rs (1)
  • exit_code (64-73)
src/ai/provider.rs (2)
src/ai/types.rs (1)
  • user (67-69)
src/ai/providers/openrouter.rs (1)
  • response (203-203)
src/safety/prompt.rs (2)
src/signals/mod.rs (2)
  • is_stdin_tty (68-70)
  • is_stdout_tty (62-64)
src/config/file.rs (5)
  • default (143-165)
  • default (170-175)
  • default (179-186)
  • default (190-195)
  • default (199-204)
src/context/gatherer.rs (4)
src/context/directory.rs (2)
  • scan_directory (18-61)
  • redact_path_internal (104-138)
src/context/history.rs (1)
  • get_shell_history (112-119)
src/context/stdin.rs (1)
  • read_stdin_default (66-69)
src/context/system.rs (1)
  • get_formatted_system_info (119-122)
src/ai/providers/openrouter.rs (2)
src/logging/file_logger.rs (1)
  • new (42-66)
src/ai/types.rs (3)
  • new (41-43)
  • new (114-121)
  • new (203-209)
src/safety/mod.rs (5)
src/safety/confirmation.rs (1)
  • format_decision (171-177)
src/safety/detector.rs (3)
  • get_matching_pattern (79-96)
  • is_dangerous_command (26-39)
  • is_dangerous_command_with_regexes (64-66)
src/safety/interactive.rs (2)
  • execute_command (236-258)
  • prompt_command_action (86-224)
src/safety/patterns.rs (2)
  • compile_dangerous_regexes (42-75)
  • get_dangerous_regexes (87-96)
src/safety/prompt.rs (3)
  • is_interactive_mode (53-55)
  • is_piped_output (63-65)
  • should_prompt (33-45)
src/safety/interactive.rs (3)
src/signals/mod.rs (1)
  • is_stderr_tty (74-76)
src/safety/confirmation.rs (1)
  • fmt (29-37)
src/config/mod.rs (1)
  • from_cli (32-71)
src/ai/types.rs (4)
src/ai/chain.rs (1)
  • new (32-47)
src/ai/providers/openrouter.rs (1)
  • new (55-66)
src/ai/prompt.rs (3)
  • serde_json (215-215)
  • serde_json (237-237)
  • serde_json (252-252)
examples/test_context.rs (1)
  • serde_json (38-38)
src/ai/handler.rs (6)
src/ai/prompt.rs (8)
  • build_chat_request (119-132)
  • build_multi_chat_request (148-182)
  • build_prompt (44-80)
  • extract_command (95-105)
  • extract_commands (196-277)
  • serde_json (215-215)
  • serde_json (237-237)
  • serde_json (252-252)
src/config/cache.rs (1)
  • get_file_config (29-45)
src/context/gatherer.rs (1)
  • gather_context (41-102)
examples/test_context.rs (1)
  • serde_json (38-38)
src/ai/chain.rs (1)
  • new (32-47)
src/main.rs (1)
  • m (75-75)
src/error/mod.rs (2)
src/ai/providers/openrouter.rs (2)
  • get_file_logger (22-24)
  • new (55-66)
src/logging/file_logger.rs (1)
  • new (42-66)
examples/test_context.rs (1)
src/context/gatherer.rs (1)
  • gather_context (41-102)
src/config/merger.rs (2)
src/config/loader.rs (1)
  • load_all_configs (134-173)
src/config/file.rs (5)
  • default (143-165)
  • default (170-175)
  • default (179-186)
  • default (190-195)
  • default (199-204)
src/lib.rs (7)
src/logging/mod.rs (1)
  • error (124-126)
src/ai/handler.rs (1)
  • generate_command (139-173)
src/cli/mod.rs (1)
  • parse_args (85-87)
src/color/mod.rs (2)
  • color_mode_from_config (68-80)
  • detect_color_auto (35-63)
src/locale/mod.rs (3)
  • get_language_code (25-37)
  • get_locale (12-14)
  • is_c_locale (44-47)
src/output/mod.rs (3)
  • format_config_debug (41-43)
  • format_output (8-10)
  • print_command (24-36)
src/signals/mod.rs (6)
  • is_interactive (81-83)
  • is_piped (88-90)
  • is_stderr_tty (74-76)
  • is_stdin_tty (68-70)
  • is_stdout_tty (62-64)
  • setup_signal_handlers (29-52)
src/ai/prompt.rs (1)
src/ai/types.rs (5)
  • new (41-43)
  • new (114-121)
  • new (203-209)
  • system (54-56)
  • user (67-69)
src/config/loader.rs (1)
src/config/paths.rs (1)
  • existing_config_paths (74-79)
src/logging/mod.rs (2)
src/color/mod.rs (1)
  • color_mode_from_config (68-80)
src/logging/file_logger.rs (1)
  • log (74-88)
🪛 LanguageTool
README.md

[grammar] ~1-~1: Ensure spelling is correct
Context: # clai A CLI tool that converts natural languag...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🪛 markdownlint-cli2 (0.18.1)
CONTRIBUTING.md

27-27: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

Comment on lines +142 to +147
// Benchmark: Signal handler setup
group.bench_function("setup_signal_handlers", |b| {
b.iter(|| {
let _flag = setup_signal_handlers();
});
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Benchmark does not reflect real-world usage due to repeated handler registration.

setup_signal_handlers() is called once at program startup in production, but the benchmark invokes it ~100 times (once per iteration). Since signal_hook::flag::register() accumulates handlers and the returned SigId values are not managed, this results in 200+ signal handlers being registered across iterations. Consider moving setup_signal_handlers() outside the benchmark loop, or excluding this benchmark since it doesn't represent realistic handler registration patterns.

🤖 Prompt for AI Agents
In @benches/startup.rs around lines 142 - 147, The benchmark repeatedly
registers signal handlers by calling setup_signal_handlers() inside the closure,
causing accumulated handlers; move the one-time call out of the iterative
benchmark so handlers are registered once (e.g., call setup_signal_handlers()
before group.bench_function) or remove this benchmark entirely; alternatively
change to a setup/teardown style (use iter_batched or a before-hook) that
ensures setup_signal_handlers() runs only once per benchmark trial rather than
per iteration, and ensure any returned SigId values from
signal_hook::flag::register() are stored or unregistered as needed.

name = "clai"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <you@example.com>"]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Update placeholder author information.

The author field contains placeholder text that should be replaced with actual author information before the PR is merged.

🔎 Proposed fix
-authors = ["Your Name <you@example.com>"]
+authors = ["Vedaant-Rajoo <vedaant@example.com>"]

Or remove the field entirely if not needed (Cargo.toml doesn't require it).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
authors = ["Your Name <you@example.com>"]
authors = ["Vedaant-Rajoo <vedaant@example.com>"]
🤖 Prompt for AI Agents
In @Cargo.toml at line 5, The authors field in Cargo.toml contains placeholder
text; either replace authors = ["Your Name <you@example.com>"] with the actual
project author(s) information (e.g., real names and contact emails) or remove
the entire authors entry if you prefer not to include it, ensuring Cargo.toml no
longer contains the placeholder string.

Comment on lines +27 to +28
```
src/
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add language identifier to fenced code block.

The code block at line 27 lacks a language identifier, causing markdown linting failures.

🔎 Proposed fix
-```
+```text
 src/
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
src/
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

27-27: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In @CONTRIBUTING.md around lines 27 - 28, The fenced code block that currently
contains just "src/" is missing a language identifier and triggers markdown
linting; update the fenced block that contains "src/" to include a language tag
(for example use "text" or "bash") after the opening triple backticks so the
block becomes a properly labeled fenced code block.

Comment on lines +62 to +65
1. `./.clai.toml` (project-local, highest priority)
2. `~/.config/clai/config.toml` (user)
3. `/etc/clai/config.toml` (system)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Document XDG_CONFIG_HOME support in config file precedence.

The implementation in src/config/paths.rs supports $XDG_CONFIG_HOME/clai/config.toml as the second priority, but this is not reflected in the documentation.

🔎 Proposed fix

Update the list to include all four locations:

 1. `./.clai.toml` (project-local, highest priority)
-2. `~/.config/clai/config.toml` (user)
-3. `/etc/clai/config.toml` (system)
+2. `$XDG_CONFIG_HOME/clai/config.toml` (if XDG_CONFIG_HOME is set)
+3. `~/.config/clai/config.toml` (fallback if XDG_CONFIG_HOME not set)
+4. `/etc/clai/config.toml` (system, lowest priority)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
1. `./.clai.toml` (project-local, highest priority)
2. `~/.config/clai/config.toml` (user)
3. `/etc/clai/config.toml` (system)
1. `./.clai.toml` (project-local, highest priority)
2. `$XDG_CONFIG_HOME/clai/config.toml` (if XDG_CONFIG_HOME is set)
3. `~/.config/clai/config.toml` (fallback if XDG_CONFIG_HOME not set)
4. `/etc/clai/config.toml` (system, lowest priority)
🤖 Prompt for AI Agents
In @CONTRIBUTING.md around lines 62 - 65, Update the CONTRIBUTING.md config
precedence list to reflect the implementation in src/config/paths.rs by adding
the XDG_CONFIG_HOME entry: include `$XDG_CONFIG_HOME/clai/config.toml` as the
second priority (after `./.clai.toml`), shifting the user and system paths
accordingly so the four locations match the code: `./.clai.toml`,
`$XDG_CONFIG_HOME/clai/config.toml`, `~/.config/clai/config.toml`, and
`/etc/clai/config.toml`.

Comment on lines +9 to +10
echo "1. Formatting code..."
cargo fmt
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use --check flag with cargo fmt in pre-commit hook.

Running cargo fmt without --check will modify files during the commit process, but those modifications won't be included in the commit. This creates confusion as the commit will contain unformatted code while the working directory has formatted code. Use --check to fail the commit if formatting is needed.

🔎 Proposed fix
 echo "1. Formatting code..."
-cargo fmt
+cargo fmt -- --check
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
echo "1. Formatting code..."
cargo fmt
echo "1. Formatting code..."
cargo fmt -- --check
🤖 Prompt for AI Agents
In @scripts/hooks/pre-commit around lines 9 - 10, The pre-commit hook currently
runs "cargo fmt" which can modify files but those edits won't be committed;
update the hook to run "cargo fmt -- --check" so the hook fails when formatting
is required instead of mutating files during commit. Modify the command in the
pre-commit script (the block containing the echo "1. Formatting code..." and the
cargo fmt invocation) to use the check flag, ensuring formatting is enforced
without changing the working tree.

Comment on lines +79 to +96
pub fn get_matching_pattern(command: &str, config: &FileConfig) -> Option<(usize, String)> {
let regexes = get_dangerous_regexes(config).ok()?;

for (index, regex) in regexes.iter().enumerate() {
if regex.is_match(command) {
// Get the original pattern from config (for display)
let pattern = config
.safety
.dangerous_patterns
.get(index)
.cloned()
.unwrap_or_else(|| format!("pattern_{}", index));
return Some((index, pattern));
}
}

None
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential index mismatch when using default patterns.

When config.safety.dangerous_patterns is empty, compile_dangerous_regexes uses default_dangerous_patterns() instead. However, get_matching_pattern always looks up the pattern from config.safety.dangerous_patterns using the regex index (lines 85-88). This will return None and fall back to a generic "pattern_{}" string, losing the actual pattern information.

🔎 Proposed fix
 pub fn get_matching_pattern(command: &str, config: &FileConfig) -> Option<(usize, String)> {
     let regexes = get_dangerous_regexes(config).ok()?;
+    
+    // Determine which patterns were actually used (default or config)
+    let patterns: &[String] = if config.safety.dangerous_patterns.is_empty() {
+        // Would need to import default_dangerous_patterns or store patterns alongside regexes
+        // For now, fallback to regex string representation
+        &[]
+    } else {
+        &config.safety.dangerous_patterns
+    };
 
     for (index, regex) in regexes.iter().enumerate() {
         if regex.is_match(command) {
-            // Get the original pattern from config (for display)
-            let pattern = config
-                .safety
-                .dangerous_patterns
+            let pattern = patterns
                 .get(index)
                 .cloned()
-                .unwrap_or_else(|| format!("pattern_{}", index));
+                .unwrap_or_else(|| regex.as_str().to_string());
             return Some((index, pattern));
         }
     }
 
     None
 }

Consider using regex.as_str() as a more informative fallback since it returns the actual regex pattern string.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn get_matching_pattern(command: &str, config: &FileConfig) -> Option<(usize, String)> {
let regexes = get_dangerous_regexes(config).ok()?;
for (index, regex) in regexes.iter().enumerate() {
if regex.is_match(command) {
// Get the original pattern from config (for display)
let pattern = config
.safety
.dangerous_patterns
.get(index)
.cloned()
.unwrap_or_else(|| format!("pattern_{}", index));
return Some((index, pattern));
}
}
None
}
pub fn get_matching_pattern(command: &str, config: &FileConfig) -> Option<(usize, String)> {
let regexes = get_dangerous_regexes(config).ok()?;
// Determine which patterns were actually used (default or config)
let patterns: &[String] = if config.safety.dangerous_patterns.is_empty() {
// Would need to import default_dangerous_patterns or store patterns alongside regexes
// For now, fallback to regex string representation
&[]
} else {
&config.safety.dangerous_patterns
};
for (index, regex) in regexes.iter().enumerate() {
if regex.is_match(command) {
let pattern = patterns
.get(index)
.cloned()
.unwrap_or_else(|| regex.as_str().to_string());
return Some((index, pattern));
}
}
None
}
🤖 Prompt for AI Agents
In @src/safety/detector.rs around lines 79 - 96, get_matching_pattern currently
looks up the display pattern by indexing config.safety.dangerous_patterns with
the regex index, which can be empty when compile_dangerous_regexes used
default_dangerous_patterns; change the fallback to use the matched regex's
pattern string (regex.as_str()) instead of the generic "pattern_{}" so the real
pattern is preserved. Locate get_matching_pattern and adjust the construction of
the pattern variable (currently using
config.safety.dangerous_patterns.get(index).cloned().unwrap_or_else(...)) to
prefer the configured pattern, but when that is None use regexes[index].as_str()
(or the loop variable `regex`) for a meaningful fallback; ensure types remain
String.

Comment on lines +56 to +70
Err(e) => {
// Log error to stderr but continue with other patterns
eprintln!(
"Warning: Invalid dangerous pattern at index {}: '{}' - {}",
index, pattern, e
);
// Return error for invalid regex (fail fast for safety)
return Err(anyhow::anyhow!(
"Failed to compile dangerous pattern '{}' at index {}: {}",
pattern,
index,
e
))
.context("Invalid regex pattern in dangerous_patterns config");
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent error handling: logs warning but fails fast.

The comment on line 57 says "Log error to stderr but continue with other patterns," but the code returns an error immediately (line 63), preventing other patterns from being processed. Either remove the warning and just return the error, or log and continue processing. The current approach is confusing.

🤖 Prompt for AI Agents
In @src/safety/patterns.rs around lines 56 - 70, The code in the dangerous
patterns compilation block logs a warning but then immediately returns an error
(in the Err(e) arm), which contradicts the "continue with other patterns"
intent; change the error handling in that match arm (the Err(e) branch) to log
the error and continue processing remaining patterns instead of returning—i.e.,
remove the return Err(...)/.context(...) and replace it with a continue (or
otherwise skip this pattern), keeping the eprintln! call as-is so invalid
regexes are warned but do not abort compilation of other patterns in the
function that builds/iterates dangerous_patterns.

Comment on lines +87 to +96
pub fn get_dangerous_regexes(config: &FileConfig) -> Result<&'static [Regex]> {
DANGEROUS_REGEXES
.get_or_init(|| match compile_dangerous_regexes(config) {
Ok(regexes) => Ok(regexes),
Err(e) => Err(e.to_string()),
})
.as_ref()
.map_err(|e| anyhow::anyhow!("Failed to compile dangerous patterns: {}", e))
.map(|regexes| regexes.as_slice())
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

OnceLock ignores config changes after first initialization.

The get_dangerous_regexes function uses OnceLock::get_or_init, which means the config passed on the first call determines the patterns forever. Subsequent calls with different configs will return the same cached patterns, silently ignoring the new config.

Comment on lines +22 to +30
test_result() {
if [ $? -eq 0 ]; then
echo -e "${GREEN}✓${NC} $1"
((TESTS_PASSED++))
else
echo -e "${RED}✗${NC} $1"
((TESTS_FAILED++))
fi
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: test_result function doesn't capture exit status correctly.

The $? check on line 23 captures the exit status of the if command itself, not the command that ran before calling test_result. This means all tests will incorrectly report as passed regardless of actual outcomes.

🔎 Proposed fix
 test_result() {
-    if [ $? -eq 0 ]; then
-        echo -e "${GREEN}✓${NC} $1"
+    local status=$1
+    local message=$2
+    if [ "$status" -eq 0 ]; then
+        echo -e "${GREEN}✓${NC} $message"
         ((TESTS_PASSED++))
     else
-        echo -e "${RED}✗${NC} $1"
+        echo -e "${RED}✗${NC} $message"
         ((TESTS_FAILED++))
     fi
 }

Then update all call sites (e.g., lines 34-35):

 OUTPUT=$(cd "$SCRIPT_DIR" && cargo r -- "test" 2>&1)
-test_result "Default config loads successfully"
+test_result $? "Default config loads successfully"

Apply this pattern to all 10 test invocations throughout the script.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @test_config.sh around lines 22 - 30, The test_result function reads $?
inside itself so it sees the status of the if statement, not the prior test;
change the API so test_result accepts an explicit exit status (e.g., first
parameter) and use that to increment TESTS_PASSED/TESTS_FAILED and print the
message (second parameter), then update all call sites (e.g., the invocations
around the existing test calls) to pass "$?" as the first arg and the
human-readable test name as the second arg for every test invocation.

default = "test"
EOF
chmod 644 .clai.toml 2>/dev/null || true
OUTPUT=$(cargo r -- "test" 2>&1 2>&1)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove redundant stderr redirect.

Line 110 has 2>&1 2>&1 which is redundant. Only one redirect is needed.

🔎 Proposed fix
-OUTPUT=$(cargo r -- "test" 2>&1 2>&1)
+OUTPUT=$(cargo r -- "test" 2>&1)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
OUTPUT=$(cargo r -- "test" 2>&1 2>&1)
OUTPUT=$(cargo r -- "test" 2>&1)
🤖 Prompt for AI Agents
In @test_config.sh at line 110, The command substitution assigned to OUTPUT uses
a duplicated stderr redirect; update the assignment (the OUTPUT=$(cargo r --
"test" 2>&1 2>&1) expression) to remove the redundant `2>&1` so only a single
`2>&1` remains, preserving the intended stdout+stderr capture.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants