Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions .github/actions/cora-review/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,24 @@ runs:
INPUT_BASE: ${{ inputs.base-branch }}
INPUT_SEVERITY: ${{ inputs.severity }}
run: |
# Set higher diff limit for CI (default 50K is too small for large PRs)
printf 'hook:\n max_diff_size: 200000\n' > .cora-ci.yaml
CORA_CONFIG=".cora-ci.yaml" cora review \
--base "$INPUT_BASE" \
--format sarif \
--severity "$INPUT_SEVERITY" \
--quiet \
> cora-results.sarif 2>cora-stderr.log; EXIT_CODE=$?
# Use project .cora.yaml if it exists; otherwise create a CI-friendly fallback
if [ ! -f .cora.yaml ]; then
printf 'hook:\n max_diff_size: 5242880\n' > .cora-ci.yaml
CORA_CONFIG=".cora-ci.yaml" cora review \
--base "$INPUT_BASE" \
--format sarif \
--severity "$INPUT_SEVERITY" \
--quiet \
> cora-results.sarif 2>cora-stderr.log; EXIT_CODE=$?
else
echo "Using project .cora.yaml for configuration"
cora review \
--base "$INPUT_BASE" \
--format sarif \
--severity "$INPUT_SEVERITY" \
--quiet \
> cora-results.sarif 2>cora-stderr.log; EXIT_CODE=$?
fi
SARIF_BYTES=$(wc -c < cora-results.sarif)
echo "Cora review complete ($SARIF_BYTES bytes, exit=$EXIT_CODE)"
if [ -s cora-stderr.log ]; then
Expand Down
91 changes: 74 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,38 +87,43 @@

## 🚀 Quick Start

### 1. Set Your API Key
### 1. Install

```bash
export OPENAI_API_KEY="sk-..."
# or
export ANTHROPIC_API_KEY="sk-ant-..."
curl -fsSL https://raw.githubusercontent.com/codecoradev/cora-cli/main/install.sh | sh
```

### 2. Initialize Config (Optional)
### 2. Set Up Authentication

```bash
cora init
cora auth login
```

### 3. Review Staged Changes
Pick your provider, enter your API key — done. Cora stores it securely in `~/.cora/auth.toml` (never committed to git).

```bash
cora review --staged
```
> **No API key yet?** Set any provider env var instead: `export OPENAI_API_KEY="..."`

### 4. Review the Last Commit
### 3. Review Your Code

```bash
# Review staged changes
cora review

# Review vs a branch
cora review --base origin/main

# Review the last commit
cora review --commit HEAD
```

### 5. Scan the Entire Project
### 4. Optional — Project Config

```bash
cora scan
cora init
```

Creates `.cora.yaml` in your project root for custom settings (focus areas, ignore patterns, hook config). The CI action automatically reads this file.

## 📖 Commands

### `cora review`
Expand Down Expand Up @@ -324,16 +329,68 @@

### Authentication

API keys can be provided via environment variable (`CORA_API_KEY`), provider-specific env vars (`OPENAI_API_KEY`, etc.), or stored in `~/.cora/auth.toml` (auto-created by `cora auth login`, permission `0600`).
API keys can be provided via environment variables, stored in `~/.cora/auth.toml` (auto-created by `cora auth login`, permission `0600`), or passed via CLI flags.

#### Interactive Login (Recommended)

```bash
# Interactive login (stores key in ~/.cora/auth.toml)
cora auth login
```

This starts an interactive setup that:

1. **Lists known providers** — OpenAI, Anthropic, Groq, Ollama, Z.AI
2. **Lets you pick one** — known providers only need an API key
3. **Or choose "custom"** — enter your own base URL, model, and API key for any OpenAI-compatible endpoint

# Or set via environment variable
export CORA_API_KEY=sk-...
Example flow:

```
$ cora auth login

🔑 Cora Auth Setup
Choose your LLM provider:

[1] openai — https://api.openai.com/v1 (model: gpt-4o-mini)
[2] anthropic — https://api.anthropic.com/v1 (model: claude-3-haiku-20240307)
[3] groq — https://api.groq.com/openai/v1 (model: llama-3.1-8b-instant)
[4] ollama — http://localhost:11434/v1 (model: llama3.1)
[5] zai — https://api.z.ai/api/coding/paas/v4 (model: glm-5.1)
[6] custom — use any OpenAI-compatible endpoint

Select provider [1-6]: 1

→ Provider: openai
→ Model: gpt-4o-mini
→ Base URL: https://api.openai.com/v1

🔑 Enter your API key: sk-...

✅ API key saved to ~/.cora/auth.toml
Provider: openai | Model: gpt-4o-mini | Base: https://api.openai.com/v1
```

#### Check Auth Status

```bash
cora auth status
```

Shows your configured provider, model, and key source.

#### Environment Variables

| Variable | Description |
|----------|-------------|
| `CORA_API_KEY` | API key (overrides all other sources) |
| `OPENAI_API_KEY` | OpenAI API key (auto-detected) |
| `ANTHROPIC_API_KEY` | Anthropic API key (auto-detected) |
| `GROQ_API_KEY` | Groq API key (auto-detected) |
| `ZAI_API_KEY` | Z.AI API key (auto-detected) |
| `CORA_PROVIDER` | Override provider name |
| `CORA_MODEL` | Override model |
| `CORA_BASE_URL` | Override API base URL |

## 🔗 CI/CD Integration

### GitHub Actions
Expand Down
156 changes: 148 additions & 8 deletions src/commands/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ use anyhow::Result;
use colored::Colorize;

use crate::config::loader;
use crate::config::providers::PRESETS;

/// Execute `auth login` — prompt for API key and save it.
/// Execute `auth login` — interactive tiered provider selection and API key setup.
pub fn execute_auth_login() -> Result<()> {
// Check if already logged in
let status = loader::auth_status()?;
Expand All @@ -26,7 +27,97 @@ pub fn execute_auth_login() -> Result<()> {
}
}

print!("🔑 Enter your API key: ");
println!();
println!("{}", "🔑 Cora Auth Setup".bold());
println!("{}", " Choose your LLM provider:".dimmed());
println!();

// List known providers
for (i, preset) in PRESETS.iter().enumerate() {
println!(
" {} {} — {} (model: {})",
format!("[{}]", i + 1).cyan().bold(),
preset.name.bold(),
preset.default_base_url.dimmed(),
preset.default_model.dimmed(),
);
}
// Custom option
println!(
" {} {} — use any OpenAI-compatible endpoint",
format!("[{}]", PRESETS.len() + 1).cyan().bold(),
"custom".bold(),
);
println!();

print!(
" {} ",
format!("Select provider [1-{}]:", PRESETS.len() + 1).bold()
);
io::stdout().flush()?;

let mut choice = String::new();
io::stdin().read_line(&mut choice)?;
let choice_num: usize = choice
.trim()
.parse()
.map_err(|_| anyhow::anyhow!("Invalid choice — enter a number"))?;

if choice_num == 0 || choice_num > PRESETS.len() + 1 {
anyhow::bail!(
"Invalid choice — pick a number between 1 and {}",
PRESETS.len() + 1
);
}

let (provider, base_url, model) = if choice_num <= PRESETS.len() {
// Known provider — just need API key
let preset = &PRESETS[choice_num - 1];
println!();
println!(
" {} {} ({})",
"→".green(),
"Provider:".bold(),
preset.name.green()
);
println!(
" {} {} ({})",
"→".green(),
"Model:".bold(),
preset.default_model.green()
);
println!(
" {} {} ({})",
"→".green(),
"Base URL:".bold(),
preset.default_base_url.dimmed()
);
println!();
(
preset.name.to_string(),
preset.default_base_url.to_string(),
preset.default_model.to_string(),
)
} else {
// Custom provider — collect all details
println!();
println!(" {} {}", "→".green(), "Custom provider setup".bold());
println!();

let provider = prompt_input(" Provider name (e.g. my-llm):")?;
let base_url = prompt_input(" Base URL (e.g. https://api.example.com/v1):")?;
let model = prompt_input(" Default model (e.g. gpt-4o):")?;

if provider.is_empty() || base_url.is_empty() || model.is_empty() {
anyhow::bail!("Provider name, base URL, and model are required for custom providers");
}

(provider, base_url, model)
};

// Collect API key
println!();
print!(" 🔑 Enter your API key: ");
io::stdout().flush()?;

let mut key = String::new();
Expand All @@ -37,35 +128,80 @@ pub fn execute_auth_login() -> Result<()> {
anyhow::bail!("API key cannot be empty");
}

// Save API key + provider info
loader::save_api_key(&key)?;
println!("{} API key saved to ~/.cora/auth.toml", "✅".green().bold());
loader::save_provider_info(&provider, &base_url, &model)?;

println!();
println!(
"{} API key saved to {}",
"✅".green().bold(),
"~/.cora/auth.toml".green()
);
println!(
"{} Provider: {} | Model: {} | Base: {}",
" ".dimmed(),
provider.bold(),
model.bold(),
base_url.dimmed()
);
println!(
"{}",
" This file is local to your machine and not committed to git.".dimmed()
);
println!();
println!(
"{} Run {} to verify, or {} to start reviewing.",
"Next:".bold(),
"cora auth status".cyan(),
"cora review".cyan()
);

Ok(())
}

/// Execute `auth status` — show whether an API key is configured.
/// Prompt for a non-empty string with label.
fn prompt_input(label: &str) -> Result<String> {
print!("{} ", label);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
Ok(input.trim().to_string())
}

/// Execute `auth status` — show whether an API key is configured and which provider.
pub fn execute_auth_status() -> Result<()> {
let status = loader::auth_status()?;

if status.has_key {
println!("{} API key is configured.", "✅".green().bold());
println!(" Source: {}", status.source);

// Show stored provider info if available
if let Some(info) = loader::load_provider_info()? {
println!();
println!(" {} {}", "Provider:".bold(), info.provider.green());
println!(" {} {}", "Model:".bold(), info.model.green());
println!(" {} {}", "Base URL:".bold(), info.base_url.dimmed());
} else {
println!(
" {} No provider info stored — run {} to set up",
"ℹ️".yellow(),
"cora auth login".cyan()
);
}
} else {
println!("{} No API key configured.", "❌".red().bold());
println!(" Set it via:");
println!(" • {} (interactive setup)", "cora auth login".cyan());
println!(" • CORA_API_KEY environment variable");
println!(" • `cora auth login` command");
println!(" • `cora review --api-key <key>` flag");
println!(" • Provider-specific env vars: OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.");
}

Ok(())
}

/// Execute `auth remove` — delete the stored API key.
/// Execute `auth remove` — delete the stored API key and provider info.
pub fn execute_auth_remove() -> Result<()> {
let status = loader::auth_status()?;
if !status.has_key && std::env::var("CORA_API_KEY").is_err() {
Expand All @@ -74,7 +210,11 @@ pub fn execute_auth_remove() -> Result<()> {
}

loader::remove_api_key()?;
println!("{} API key removed from local config.", "✅".green().bold());
loader::remove_provider_info()?;
println!(
"{} API key and provider info removed from local config.",
"✅".green().bold()
);
println!(
"{}",
" If you set CORA_API_KEY in your shell, remove it there too.".dimmed()
Expand Down
Loading
Loading