feat: make rusty-claude-cli usable end-to-end

Wire the CLI to the Anthropic client, runtime conversation loop, and MVP in-tree tool executor so prompt mode and the default REPL both execute real turns instead of scaffold-only commands.

Constraint: Proxy auth uses ANTHROPIC_AUTH_TOKEN as the primary x-api-key source and may stream extra usage fields
Constraint: Must preserve existing scaffold commands while enabling real prompt and REPL flows
Rejected: Keep prompt mode on the old scaffold path | does not satisfy end-to-end CLI requirement
Rejected: Depend solely on raw SSE message_stop from proxy | proxy/event differences required tolerant parsing plus fallback handling
Confidence: medium
Scope-risk: moderate
Reversibility: clean
Directive: Keep prompt mode tool-free unless the one-shot path is explicitly expanded and reverified against the proxy
Tested: cargo test -p api; cargo test -p tools; cargo test -p runtime; cargo test -p rusty-claude-cli; cargo build; cargo run -p rusty-claude-cli -- prompt "say hello"; printf '/quit\n' | cargo run -p rusty-claude-cli --
Not-tested: Full interactive tool_use roundtrip against the proxy in REPL mode
This commit is contained in:
Yeachan-Heo
2026-03-31 18:39:39 +00:00
parent 450556559a
commit 3faf8dd365
13 changed files with 2801 additions and 78 deletions

View File

@@ -41,14 +41,12 @@ impl AnthropicClient {
}
pub fn from_env() -> Result<Self, ApiError> {
Ok(Self::new(read_api_key(|key| std::env::var(key))?)
.with_auth_token(std::env::var("ANTHROPIC_AUTH_TOKEN").ok())
.with_base_url(
std::env::var("ANTHROPIC_BASE_URL")
.ok()
.or_else(|| std::env::var("CLAUDE_CODE_API_BASE_URL").ok())
.unwrap_or_else(|| DEFAULT_BASE_URL.to_string()),
))
Ok(Self::new(read_api_key()?).with_base_url(
std::env::var("ANTHROPIC_BASE_URL")
.ok()
.or_else(|| std::env::var("CLAUDE_CODE_API_BASE_URL").ok())
.unwrap_or_else(|| DEFAULT_BASE_URL.to_string()),
))
}
#[must_use]
@@ -187,13 +185,16 @@ impl AnthropicClient {
}
}
fn read_api_key(
getter: impl FnOnce(&str) -> Result<String, std::env::VarError>,
) -> Result<String, ApiError> {
match getter("ANTHROPIC_API_KEY") {
Ok(api_key) if api_key.is_empty() => Err(ApiError::MissingApiKey),
Ok(api_key) => Ok(api_key),
Err(std::env::VarError::NotPresent) => Err(ApiError::MissingApiKey),
fn read_api_key() -> Result<String, ApiError> {
match std::env::var("ANTHROPIC_AUTH_TOKEN") {
Ok(api_key) if !api_key.is_empty() => Ok(api_key),
Ok(_) => Err(ApiError::MissingApiKey),
Err(std::env::VarError::NotPresent) => match std::env::var("ANTHROPIC_API_KEY") {
Ok(api_key) if !api_key.is_empty() => Ok(api_key),
Ok(_) => Err(ApiError::MissingApiKey),
Err(std::env::VarError::NotPresent) => Err(ApiError::MissingApiKey),
Err(error) => Err(ApiError::from(error)),
},
Err(error) => Err(ApiError::from(error)),
}
}
@@ -289,8 +290,6 @@ struct AnthropicErrorBody {
#[cfg(test)]
mod tests {
use std::env::VarError;
use super::{ALT_REQUEST_ID_HEADER, REQUEST_ID_HEADER};
use std::time::Duration;
@@ -298,21 +297,30 @@ mod tests {
#[test]
fn read_api_key_requires_presence() {
let error = super::read_api_key(|_| Err(VarError::NotPresent))
.expect_err("missing key should error");
std::env::remove_var("ANTHROPIC_AUTH_TOKEN");
std::env::remove_var("ANTHROPIC_API_KEY");
let error = super::read_api_key().expect_err("missing key should error");
assert!(matches!(error, crate::error::ApiError::MissingApiKey));
}
#[test]
fn read_api_key_requires_non_empty_value() {
let error = super::read_api_key(|_| Ok(String::new())).expect_err("empty key should error");
std::env::set_var("ANTHROPIC_AUTH_TOKEN", "");
std::env::remove_var("ANTHROPIC_API_KEY");
let error = super::read_api_key().expect_err("empty key should error");
assert!(matches!(error, crate::error::ApiError::MissingApiKey));
}
#[test]
fn with_auth_token_drops_empty_values() {
let client = super::AnthropicClient::new("test-key").with_auth_token(Some(String::new()));
assert!(client.auth_token.is_none());
fn read_api_key_prefers_auth_token() {
std::env::set_var("ANTHROPIC_AUTH_TOKEN", "auth-token");
std::env::set_var("ANTHROPIC_API_KEY", "legacy-key");
assert_eq!(
super::read_api_key().expect("token should load"),
"auth-token"
);
std::env::remove_var("ANTHROPIC_AUTH_TOKEN");
std::env::remove_var("ANTHROPIC_API_KEY");
}
#[test]