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

@@ -1,4 +1,4 @@
use std::io::{self, Write};
use std::io::{self, IsTerminal, Write};
use crossterm::cursor::MoveToColumn;
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
@@ -100,6 +100,10 @@ impl LineEditor {
}
pub fn read_line(&self) -> io::Result<Option<String>> {
if !io::stdin().is_terminal() || !io::stdout().is_terminal() {
return self.read_line_fallback();
}
enable_raw_mode()?;
let mut stdout = io::stdout();
let mut input = InputBuffer::new();
@@ -125,6 +129,23 @@ impl LineEditor {
}
}
fn read_line_fallback(&self) -> io::Result<Option<String>> {
let mut stdout = io::stdout();
write!(stdout, "{}", self.prompt)?;
stdout.flush()?;
let mut buffer = String::new();
let bytes_read = io::stdin().read_line(&mut buffer)?;
if bytes_read == 0 {
return Ok(None);
}
while matches!(buffer.chars().last(), Some('\n' | '\r')) {
buffer.pop();
}
Ok(Some(buffer))
}
fn handle_key(key: KeyEvent, input: &mut InputBuffer) -> EditorAction {
match key {
KeyEvent {