2026-03-31 18:39:39 +00:00
mod input ;
mod render ;
2026-03-31 23:38:53 +00:00
use std ::collections ::{ BTreeMap , BTreeSet } ;
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
use std ::env ;
2026-03-31 19:57:38 +00:00
use std ::fs ;
2026-03-31 23:38:05 +00:00
use std ::io ::{ self , Read , Write } ;
use std ::net ::TcpListener ;
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
use std ::path ::{ Path , PathBuf } ;
2026-03-31 23:38:05 +00:00
use std ::process ::Command ;
2026-03-31 22:49:50 +00:00
use std ::time ::{ SystemTime , UNIX_EPOCH } ;
feat: Rust port of Claude Code CLI
Crates:
- api: Anthropic Messages API client with SSE streaming
- tools: Claude-compatible tool implementations (Bash, Read, Write, Edit, Glob, Grep + extended suite)
- runtime: conversation loop, session persistence, permissions, system prompt builder
- rusty-claude-cli: terminal UI with markdown rendering, syntax highlighting, spinners
- commands: subcommand definitions
- compat-harness: upstream TS parity verification
All crates pass cargo fmt/clippy/test.
2026-03-31 17:43:09 +00:00
2026-03-31 18:39:39 +00:00
use api ::{
2026-03-31 23:38:05 +00:00
resolve_saved_oauth_token , AnthropicClient , AuthSource , ContentBlockDelta , InputContentBlock ,
InputMessage , MessageRequest , MessageResponse , OutputContentBlock ,
StreamEvent as ApiStreamEvent , ToolChoice , ToolDefinition , ToolResultContentBlock ,
2026-03-31 18:39:39 +00:00
} ;
2026-03-31 21:15:37 +00:00
use commands ::{ render_slash_command_help , resume_supported_slash_commands , SlashCommand } ;
feat: Rust port of Claude Code CLI
Crates:
- api: Anthropic Messages API client with SSE streaming
- tools: Claude-compatible tool implementations (Bash, Read, Write, Edit, Glob, Grep + extended suite)
- runtime: conversation loop, session persistence, permissions, system prompt builder
- rusty-claude-cli: terminal UI with markdown rendering, syntax highlighting, spinners
- commands: subcommand definitions
- compat-harness: upstream TS parity verification
All crates pass cargo fmt/clippy/test.
2026-03-31 17:43:09 +00:00
use compat_harness ::{ extract_manifest , UpstreamPaths } ;
2026-03-31 18:39:39 +00:00
use render ::{ Spinner , TerminalRenderer } ;
use runtime ::{
2026-03-31 23:38:05 +00:00
clear_oauth_credentials , generate_pkce_pair , generate_state , load_system_prompt ,
parse_oauth_callback_request_target , save_oauth_credentials , ApiClient , ApiRequest ,
AssistantEvent , CompactionConfig , ConfigLoader , ConfigSource , ContentBlock ,
ConversationMessage , ConversationRuntime , MessageRole , OAuthAuthorizationRequest ,
OAuthTokenExchangeRequest , PermissionMode , PermissionPolicy , ProjectContext , RuntimeError ,
Session , TokenUsage , ToolError , ToolExecutor , UsageTracker ,
2026-03-31 18:39:39 +00:00
} ;
2026-03-31 22:49:50 +00:00
use serde_json ::json ;
2026-03-31 18:39:39 +00:00
use tools ::{ execute_tool , mvp_tool_specs } ;
const DEFAULT_MODEL : & str = " claude-sonnet-4-20250514 " ;
const DEFAULT_MAX_TOKENS : u32 = 32 ;
const DEFAULT_DATE : & str = " 2026-03-31 " ;
2026-03-31 23:38:05 +00:00
const DEFAULT_OAUTH_CALLBACK_PORT : u16 = 4545 ;
2026-03-31 22:49:50 +00:00
const VERSION : & str = env! ( " CARGO_PKG_VERSION " ) ;
const BUILD_TARGET : Option < & str > = option_env! ( " TARGET " ) ;
const GIT_SHA : Option < & str > = option_env! ( " GIT_SHA " ) ;
feat: Rust port of Claude Code CLI
Crates:
- api: Anthropic Messages API client with SSE streaming
- tools: Claude-compatible tool implementations (Bash, Read, Write, Edit, Glob, Grep + extended suite)
- runtime: conversation loop, session persistence, permissions, system prompt builder
- rusty-claude-cli: terminal UI with markdown rendering, syntax highlighting, spinners
- commands: subcommand definitions
- compat-harness: upstream TS parity verification
All crates pass cargo fmt/clippy/test.
2026-03-31 17:43:09 +00:00
2026-03-31 23:38:53 +00:00
type AllowedToolSet = BTreeSet < String > ;
feat: Rust port of Claude Code CLI
Crates:
- api: Anthropic Messages API client with SSE streaming
- tools: Claude-compatible tool implementations (Bash, Read, Write, Edit, Glob, Grep + extended suite)
- runtime: conversation loop, session persistence, permissions, system prompt builder
- rusty-claude-cli: terminal UI with markdown rendering, syntax highlighting, spinners
- commands: subcommand definitions
- compat-harness: upstream TS parity verification
All crates pass cargo fmt/clippy/test.
2026-03-31 17:43:09 +00:00
fn main ( ) {
2026-03-31 18:39:39 +00:00
if let Err ( error ) = run ( ) {
2026-03-31 22:49:50 +00:00
eprintln! (
" error: {error}
Run ` rusty - claude - cli - - help ` for usage . "
) ;
2026-03-31 18:39:39 +00:00
std ::process ::exit ( 1 ) ;
}
}
feat: Rust port of Claude Code CLI
Crates:
- api: Anthropic Messages API client with SSE streaming
- tools: Claude-compatible tool implementations (Bash, Read, Write, Edit, Glob, Grep + extended suite)
- runtime: conversation loop, session persistence, permissions, system prompt builder
- rusty-claude-cli: terminal UI with markdown rendering, syntax highlighting, spinners
- commands: subcommand definitions
- compat-harness: upstream TS parity verification
All crates pass cargo fmt/clippy/test.
2026-03-31 17:43:09 +00:00
2026-03-31 18:39:39 +00:00
fn run ( ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
let args : Vec < String > = env ::args ( ) . skip ( 1 ) . collect ( ) ;
match parse_args ( & args ) ? {
CliAction ::DumpManifests = > dump_manifests ( ) ,
CliAction ::BootstrapPlan = > print_bootstrap_plan ( ) ,
CliAction ::PrintSystemPrompt { cwd , date } = > print_system_prompt ( cwd , date ) ,
2026-03-31 23:38:53 +00:00
CliAction ::Version = > print_version ( ) ,
2026-03-31 18:39:39 +00:00
CliAction ::ResumeSession {
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
session_path ,
2026-03-31 20:00:13 +00:00
commands ,
} = > resume_session ( & session_path , & commands ) ,
2026-03-31 22:49:50 +00:00
CliAction ::Prompt {
prompt ,
model ,
output_format ,
2026-03-31 23:38:53 +00:00
allowed_tools ,
} = > LiveCli ::new ( model , false , allowed_tools ) ?
. run_turn_with_output ( & prompt , output_format ) ? ,
2026-03-31 23:38:05 +00:00
CliAction ::Login = > run_login ( ) ? ,
CliAction ::Logout = > run_logout ( ) ? ,
2026-03-31 23:38:53 +00:00
CliAction ::Repl {
model ,
allowed_tools ,
} = > run_repl ( model , allowed_tools ) ? ,
2026-03-31 18:39:39 +00:00
CliAction ::Help = > print_help ( ) ,
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
}
2026-03-31 18:39:39 +00:00
Ok ( ( ) )
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
}
#[ derive(Debug, Clone, PartialEq, Eq) ]
enum CliAction {
DumpManifests ,
BootstrapPlan ,
PrintSystemPrompt {
cwd : PathBuf ,
date : String ,
} ,
2026-03-31 23:38:53 +00:00
Version ,
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
ResumeSession {
session_path : PathBuf ,
2026-03-31 20:00:13 +00:00
commands : Vec < String > ,
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
} ,
2026-03-31 18:39:39 +00:00
Prompt {
prompt : String ,
model : String ,
2026-03-31 22:49:50 +00:00
output_format : CliOutputFormat ,
2026-03-31 23:38:53 +00:00
allowed_tools : Option < AllowedToolSet > ,
2026-03-31 18:39:39 +00:00
} ,
2026-03-31 23:38:05 +00:00
Login ,
Logout ,
2026-03-31 18:39:39 +00:00
Repl {
model : String ,
2026-03-31 23:38:53 +00:00
allowed_tools : Option < AllowedToolSet > ,
2026-03-31 18:39:39 +00:00
} ,
2026-03-31 22:49:50 +00:00
// prompt-mode formatting is only supported for non-interactive runs
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
Help ,
}
feat: Rust port of Claude Code CLI
Crates:
- api: Anthropic Messages API client with SSE streaming
- tools: Claude-compatible tool implementations (Bash, Read, Write, Edit, Glob, Grep + extended suite)
- runtime: conversation loop, session persistence, permissions, system prompt builder
- rusty-claude-cli: terminal UI with markdown rendering, syntax highlighting, spinners
- commands: subcommand definitions
- compat-harness: upstream TS parity verification
All crates pass cargo fmt/clippy/test.
2026-03-31 17:43:09 +00:00
2026-03-31 22:49:50 +00:00
#[ derive(Debug, Clone, Copy, PartialEq, Eq) ]
enum CliOutputFormat {
Text ,
Json ,
}
impl CliOutputFormat {
fn parse ( value : & str ) -> Result < Self , String > {
match value {
" text " = > Ok ( Self ::Text ) ,
" json " = > Ok ( Self ::Json ) ,
other = > Err ( format! (
" unsupported value for --output-format: {other} (expected text or json) "
) ) ,
}
}
}
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
fn parse_args ( args : & [ String ] ) -> Result < CliAction , String > {
2026-03-31 18:39:39 +00:00
let mut model = DEFAULT_MODEL . to_string ( ) ;
2026-03-31 22:49:50 +00:00
let mut output_format = CliOutputFormat ::Text ;
2026-03-31 23:38:53 +00:00
let mut wants_version = false ;
let mut allowed_tool_values = Vec ::new ( ) ;
2026-03-31 18:39:39 +00:00
let mut rest = Vec ::new ( ) ;
let mut index = 0 ;
while index < args . len ( ) {
match args [ index ] . as_str ( ) {
2026-03-31 23:38:53 +00:00
" --version " | " -V " = > {
wants_version = true ;
index + = 1 ;
}
2026-03-31 18:39:39 +00:00
" --model " = > {
let value = args
. get ( index + 1 )
. ok_or_else ( | | " missing value for --model " . to_string ( ) ) ? ;
2026-03-31 19:23:05 +00:00
model . clone_from ( value ) ;
2026-03-31 18:39:39 +00:00
index + = 2 ;
}
flag if flag . starts_with ( " --model= " ) = > {
model = flag [ 8 .. ] . to_string ( ) ;
index + = 1 ;
}
2026-03-31 22:49:50 +00:00
" --output-format " = > {
let value = args
. get ( index + 1 )
. ok_or_else ( | | " missing value for --output-format " . to_string ( ) ) ? ;
output_format = CliOutputFormat ::parse ( value ) ? ;
index + = 2 ;
}
flag if flag . starts_with ( " --output-format= " ) = > {
output_format = CliOutputFormat ::parse ( & flag [ 16 .. ] ) ? ;
index + = 1 ;
}
2026-03-31 23:38:53 +00:00
" --allowedTools " | " --allowed-tools " = > {
let value = args
. get ( index + 1 )
. ok_or_else ( | | " missing value for --allowedTools " . to_string ( ) ) ? ;
allowed_tool_values . push ( value . clone ( ) ) ;
index + = 2 ;
}
flag if flag . starts_with ( " --allowedTools= " ) = > {
allowed_tool_values . push ( flag [ 15 .. ] . to_string ( ) ) ;
index + = 1 ;
}
flag if flag . starts_with ( " --allowed-tools= " ) = > {
allowed_tool_values . push ( flag [ 16 .. ] . to_string ( ) ) ;
index + = 1 ;
}
2026-03-31 18:39:39 +00:00
other = > {
rest . push ( other . to_string ( ) ) ;
index + = 1 ;
}
}
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
}
2026-03-31 23:38:53 +00:00
if wants_version {
return Ok ( CliAction ::Version ) ;
}
let allowed_tools = normalize_allowed_tools ( & allowed_tool_values ) ? ;
2026-03-31 18:39:39 +00:00
if rest . is_empty ( ) {
2026-03-31 23:38:53 +00:00
return Ok ( CliAction ::Repl {
model ,
allowed_tools ,
} ) ;
2026-03-31 18:39:39 +00:00
}
if matches! ( rest . first ( ) . map ( String ::as_str ) , Some ( " --help " | " -h " ) ) {
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
return Ok ( CliAction ::Help ) ;
}
2026-03-31 18:39:39 +00:00
if rest . first ( ) . map ( String ::as_str ) = = Some ( " --resume " ) {
return parse_resume_args ( & rest [ 1 .. ] ) ;
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
}
2026-03-31 18:39:39 +00:00
match rest [ 0 ] . as_str ( ) {
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
" dump-manifests " = > Ok ( CliAction ::DumpManifests ) ,
" bootstrap-plan " = > Ok ( CliAction ::BootstrapPlan ) ,
2026-03-31 18:39:39 +00:00
" system-prompt " = > parse_system_prompt_args ( & rest [ 1 .. ] ) ,
2026-03-31 23:38:05 +00:00
" login " = > Ok ( CliAction ::Login ) ,
" logout " = > Ok ( CliAction ::Logout ) ,
2026-03-31 18:39:39 +00:00
" prompt " = > {
let prompt = rest [ 1 .. ] . join ( " " ) ;
if prompt . trim ( ) . is_empty ( ) {
return Err ( " prompt subcommand requires a prompt string " . to_string ( ) ) ;
}
2026-03-31 22:49:50 +00:00
Ok ( CliAction ::Prompt {
prompt ,
model ,
output_format ,
2026-03-31 23:38:53 +00:00
allowed_tools ,
2026-03-31 22:49:50 +00:00
} )
2026-03-31 18:39:39 +00:00
}
2026-03-31 22:49:50 +00:00
other if ! other . starts_with ( '/' ) = > Ok ( CliAction ::Prompt {
prompt : rest . join ( " " ) ,
model ,
output_format ,
2026-03-31 23:38:53 +00:00
allowed_tools ,
2026-03-31 22:49:50 +00:00
} ) ,
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
other = > Err ( format! ( " unknown subcommand: {other} " ) ) ,
feat: Rust port of Claude Code CLI
Crates:
- api: Anthropic Messages API client with SSE streaming
- tools: Claude-compatible tool implementations (Bash, Read, Write, Edit, Glob, Grep + extended suite)
- runtime: conversation loop, session persistence, permissions, system prompt builder
- rusty-claude-cli: terminal UI with markdown rendering, syntax highlighting, spinners
- commands: subcommand definitions
- compat-harness: upstream TS parity verification
All crates pass cargo fmt/clippy/test.
2026-03-31 17:43:09 +00:00
}
}
2026-03-31 23:38:53 +00:00
fn normalize_allowed_tools ( values : & [ String ] ) -> Result < Option < AllowedToolSet > , String > {
if values . is_empty ( ) {
return Ok ( None ) ;
}
let canonical_names = mvp_tool_specs ( )
. into_iter ( )
. map ( | spec | spec . name . to_string ( ) )
. collect ::< Vec < _ > > ( ) ;
let mut name_map = canonical_names
. iter ( )
. map ( | name | ( normalize_tool_name ( name ) , name . clone ( ) ) )
. collect ::< BTreeMap < _ , _ > > ( ) ;
for ( alias , canonical ) in [
( " read " , " read_file " ) ,
( " write " , " write_file " ) ,
( " edit " , " edit_file " ) ,
( " glob " , " glob_search " ) ,
( " grep " , " grep_search " ) ,
] {
name_map . insert ( alias . to_string ( ) , canonical . to_string ( ) ) ;
}
let mut allowed = AllowedToolSet ::new ( ) ;
for value in values {
for token in value
. split ( | ch : char | ch = = ',' | | ch . is_whitespace ( ) )
. filter ( | token | ! token . is_empty ( ) )
{
let normalized = normalize_tool_name ( token ) ;
let canonical = name_map . get ( & normalized ) . ok_or_else ( | | {
format! (
" unsupported tool in --allowedTools: {token} (expected one of: {}) " ,
canonical_names . join ( " , " )
)
} ) ? ;
allowed . insert ( canonical . clone ( ) ) ;
}
}
Ok ( Some ( allowed ) )
}
fn normalize_tool_name ( value : & str ) -> String {
value . trim ( ) . replace ( '-' , " _ " ) . to_ascii_lowercase ( )
}
fn filter_tool_specs ( allowed_tools : Option < & AllowedToolSet > ) -> Vec < tools ::ToolSpec > {
mvp_tool_specs ( )
. into_iter ( )
. filter ( | spec | allowed_tools . is_none_or ( | allowed | allowed . contains ( spec . name ) ) )
. collect ( )
}
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
fn parse_system_prompt_args ( args : & [ String ] ) -> Result < CliAction , String > {
let mut cwd = env ::current_dir ( ) . map_err ( | error | error . to_string ( ) ) ? ;
2026-03-31 18:39:39 +00:00
let mut date = DEFAULT_DATE . to_string ( ) ;
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
let mut index = 0 ;
while index < args . len ( ) {
match args [ index ] . as_str ( ) {
" --cwd " = > {
let value = args
. get ( index + 1 )
. ok_or_else ( | | " missing value for --cwd " . to_string ( ) ) ? ;
cwd = PathBuf ::from ( value ) ;
index + = 2 ;
}
" --date " = > {
let value = args
. get ( index + 1 )
. ok_or_else ( | | " missing value for --date " . to_string ( ) ) ? ;
date . clone_from ( value ) ;
index + = 2 ;
}
other = > return Err ( format! ( " unknown system-prompt option: {other} " ) ) ,
}
feat: Rust port of Claude Code CLI
Crates:
- api: Anthropic Messages API client with SSE streaming
- tools: Claude-compatible tool implementations (Bash, Read, Write, Edit, Glob, Grep + extended suite)
- runtime: conversation loop, session persistence, permissions, system prompt builder
- rusty-claude-cli: terminal UI with markdown rendering, syntax highlighting, spinners
- commands: subcommand definitions
- compat-harness: upstream TS parity verification
All crates pass cargo fmt/clippy/test.
2026-03-31 17:43:09 +00:00
}
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
Ok ( CliAction ::PrintSystemPrompt { cwd , date } )
feat: Rust port of Claude Code CLI
Crates:
- api: Anthropic Messages API client with SSE streaming
- tools: Claude-compatible tool implementations (Bash, Read, Write, Edit, Glob, Grep + extended suite)
- runtime: conversation loop, session persistence, permissions, system prompt builder
- rusty-claude-cli: terminal UI with markdown rendering, syntax highlighting, spinners
- commands: subcommand definitions
- compat-harness: upstream TS parity verification
All crates pass cargo fmt/clippy/test.
2026-03-31 17:43:09 +00:00
}
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
fn parse_resume_args ( args : & [ String ] ) -> Result < CliAction , String > {
let session_path = args
. first ( )
. ok_or_else ( | | " missing session path for --resume " . to_string ( ) )
. map ( PathBuf ::from ) ? ;
2026-03-31 20:00:13 +00:00
let commands = args [ 1 .. ] . to_vec ( ) ;
if commands
. iter ( )
. any ( | command | ! command . trim_start ( ) . starts_with ( '/' ) )
{
return Err ( " --resume trailing arguments must be slash commands " . to_string ( ) ) ;
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
}
Ok ( CliAction ::ResumeSession {
session_path ,
2026-03-31 20:00:13 +00:00
commands ,
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
} )
}
fn dump_manifests ( ) {
feat: Rust port of Claude Code CLI
Crates:
- api: Anthropic Messages API client with SSE streaming
- tools: Claude-compatible tool implementations (Bash, Read, Write, Edit, Glob, Grep + extended suite)
- runtime: conversation loop, session persistence, permissions, system prompt builder
- rusty-claude-cli: terminal UI with markdown rendering, syntax highlighting, spinners
- commands: subcommand definitions
- compat-harness: upstream TS parity verification
All crates pass cargo fmt/clippy/test.
2026-03-31 17:43:09 +00:00
let workspace_dir = PathBuf ::from ( env! ( " CARGO_MANIFEST_DIR " ) ) . join ( " ../.. " ) ;
let paths = UpstreamPaths ::from_workspace_dir ( & workspace_dir ) ;
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
match extract_manifest ( & paths ) {
Ok ( manifest ) = > {
println! ( " commands: {} " , manifest . commands . entries ( ) . len ( ) ) ;
println! ( " tools: {} " , manifest . tools . entries ( ) . len ( ) ) ;
println! ( " bootstrap phases: {} " , manifest . bootstrap . phases ( ) . len ( ) ) ;
}
Err ( error ) = > {
eprintln! ( " failed to extract manifests: {error} " ) ;
std ::process ::exit ( 1 ) ;
}
}
feat: Rust port of Claude Code CLI
Crates:
- api: Anthropic Messages API client with SSE streaming
- tools: Claude-compatible tool implementations (Bash, Read, Write, Edit, Glob, Grep + extended suite)
- runtime: conversation loop, session persistence, permissions, system prompt builder
- rusty-claude-cli: terminal UI with markdown rendering, syntax highlighting, spinners
- commands: subcommand definitions
- compat-harness: upstream TS parity verification
All crates pass cargo fmt/clippy/test.
2026-03-31 17:43:09 +00:00
}
fn print_bootstrap_plan ( ) {
2026-03-31 18:39:39 +00:00
for phase in runtime ::BootstrapPlan ::claude_code_default ( ) . phases ( ) {
feat: Rust port of Claude Code CLI
Crates:
- api: Anthropic Messages API client with SSE streaming
- tools: Claude-compatible tool implementations (Bash, Read, Write, Edit, Glob, Grep + extended suite)
- runtime: conversation loop, session persistence, permissions, system prompt builder
- rusty-claude-cli: terminal UI with markdown rendering, syntax highlighting, spinners
- commands: subcommand definitions
- compat-harness: upstream TS parity verification
All crates pass cargo fmt/clippy/test.
2026-03-31 17:43:09 +00:00
println! ( " - {phase:?} " ) ;
}
}
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
2026-03-31 23:38:05 +00:00
fn run_login ( ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
let cwd = env ::current_dir ( ) ? ;
let config = ConfigLoader ::default_for ( & cwd ) . load ( ) ? ;
let oauth = config . oauth ( ) . ok_or_else ( | | {
io ::Error ::new (
io ::ErrorKind ::NotFound ,
" OAuth config is missing. Add settings.oauth.clientId/authorizeUrl/tokenUrl first. " ,
)
} ) ? ;
let callback_port = oauth . callback_port . unwrap_or ( DEFAULT_OAUTH_CALLBACK_PORT ) ;
let redirect_uri = runtime ::loopback_redirect_uri ( callback_port ) ;
let pkce = generate_pkce_pair ( ) ? ;
let state = generate_state ( ) ? ;
let authorize_url =
OAuthAuthorizationRequest ::from_config ( oauth , redirect_uri . clone ( ) , state . clone ( ) , & pkce )
. build_url ( ) ;
println! ( " Starting Claude OAuth login... " ) ;
println! ( " Listening for callback on {redirect_uri} " ) ;
if let Err ( error ) = open_browser ( & authorize_url ) {
eprintln! ( " warning: failed to open browser automatically: {error} " ) ;
println! ( " Open this URL manually: \n {authorize_url} " ) ;
}
let callback = wait_for_oauth_callback ( callback_port ) ? ;
if let Some ( error ) = callback . error {
let description = callback
. error_description
. unwrap_or_else ( | | " authorization failed " . to_string ( ) ) ;
return Err ( io ::Error ::other ( format! ( " {error} : {description} " ) ) . into ( ) ) ;
}
let code = callback . code . ok_or_else ( | | {
io ::Error ::new ( io ::ErrorKind ::InvalidData , " callback did not include code " )
} ) ? ;
let returned_state = callback . state . ok_or_else ( | | {
io ::Error ::new ( io ::ErrorKind ::InvalidData , " callback did not include state " )
} ) ? ;
if returned_state ! = state {
return Err ( io ::Error ::new ( io ::ErrorKind ::InvalidData , " oauth state mismatch " ) . into ( ) ) ;
}
let client = AnthropicClient ::from_auth ( AuthSource ::None ) ;
let exchange_request =
OAuthTokenExchangeRequest ::from_config ( oauth , code , state , pkce . verifier , redirect_uri ) ;
let runtime = tokio ::runtime ::Runtime ::new ( ) ? ;
let token_set = runtime . block_on ( client . exchange_oauth_code ( oauth , & exchange_request ) ) ? ;
save_oauth_credentials ( & runtime ::OAuthTokenSet {
access_token : token_set . access_token ,
refresh_token : token_set . refresh_token ,
expires_at : token_set . expires_at ,
scopes : token_set . scopes ,
} ) ? ;
println! ( " Claude OAuth login complete. " ) ;
Ok ( ( ) )
}
fn run_logout ( ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
clear_oauth_credentials ( ) ? ;
println! ( " Claude OAuth credentials cleared. " ) ;
Ok ( ( ) )
}
fn open_browser ( url : & str ) -> io ::Result < ( ) > {
let commands = if cfg! ( target_os = " macos " ) {
vec! [ ( " open " , vec! [ url ] ) ]
} else if cfg! ( target_os = " windows " ) {
vec! [ ( " cmd " , vec! [ " /C " , " start " , " " , url ] ) ]
} else {
vec! [ ( " xdg-open " , vec! [ url ] ) ]
} ;
for ( program , args ) in commands {
match Command ::new ( program ) . args ( args ) . spawn ( ) {
Ok ( _ ) = > return Ok ( ( ) ) ,
Err ( error ) if error . kind ( ) = = io ::ErrorKind ::NotFound = > { }
Err ( error ) = > return Err ( error ) ,
}
}
Err ( io ::Error ::new (
io ::ErrorKind ::NotFound ,
" no supported browser opener command found " ,
) )
}
fn wait_for_oauth_callback (
port : u16 ,
) -> Result < runtime ::OAuthCallbackParams , Box < dyn std ::error ::Error > > {
let listener = TcpListener ::bind ( ( " 127.0.0.1 " , port ) ) ? ;
let ( mut stream , _ ) = listener . accept ( ) ? ;
let mut buffer = [ 0_ u8 ; 4096 ] ;
let bytes_read = stream . read ( & mut buffer ) ? ;
let request = String ::from_utf8_lossy ( & buffer [ .. bytes_read ] ) ;
let request_line = request . lines ( ) . next ( ) . ok_or_else ( | | {
io ::Error ::new ( io ::ErrorKind ::InvalidData , " missing callback request line " )
} ) ? ;
let target = request_line . split_whitespace ( ) . nth ( 1 ) . ok_or_else ( | | {
io ::Error ::new (
io ::ErrorKind ::InvalidData ,
" missing callback request target " ,
)
} ) ? ;
let callback = parse_oauth_callback_request_target ( target )
. map_err ( | error | io ::Error ::new ( io ::ErrorKind ::InvalidData , error ) ) ? ;
let body = if callback . error . is_some ( ) {
" Claude OAuth login failed. You can close this window. "
} else {
" Claude OAuth login succeeded. You can close this window. "
} ;
let response = format! (
" HTTP/1.1 200 OK \r \n content-type: text/plain; charset=utf-8 \r \n content-length: {} \r \n connection: close \r \n \r \n {} " ,
body . len ( ) ,
body
) ;
stream . write_all ( response . as_bytes ( ) ) ? ;
Ok ( callback )
}
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
fn print_system_prompt ( cwd : PathBuf , date : String ) {
match load_system_prompt ( cwd , date , env ::consts ::OS , " unknown " ) {
Ok ( sections ) = > println! ( " {} " , sections . join ( " \n \n " ) ) ,
Err ( error ) = > {
eprintln! ( " failed to build system prompt: {error} " ) ;
std ::process ::exit ( 1 ) ;
}
}
}
2026-03-31 23:38:53 +00:00
fn print_version ( ) {
println! ( " {} " , render_version_report ( ) ) ;
}
2026-03-31 20:00:13 +00:00
fn resume_session ( session_path : & Path , commands : & [ String ] ) {
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
let session = match Session ::load_from_path ( session_path ) {
Ok ( session ) = > session ,
Err ( error ) = > {
eprintln! ( " failed to restore session: {error} " ) ;
std ::process ::exit ( 1 ) ;
}
} ;
2026-03-31 20:00:13 +00:00
if commands . is_empty ( ) {
println! (
" Restored session from {} ({} messages). " ,
session_path . display ( ) ,
session . messages . len ( )
) ;
return ;
}
let mut session = session ;
for raw_command in commands {
let Some ( command ) = SlashCommand ::parse ( raw_command ) else {
eprintln! ( " unsupported resumed command: {raw_command} " ) ;
std ::process ::exit ( 2 ) ;
} ;
match run_resume_command ( session_path , & session , & command ) {
Ok ( ResumeCommandOutcome {
session : next_session ,
message ,
} ) = > {
session = next_session ;
if let Some ( message ) = message {
println! ( " {message} " ) ;
}
}
2026-03-31 19:54:09 +00:00
Err ( error ) = > {
eprintln! ( " {error} " ) ;
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
std ::process ::exit ( 2 ) ;
}
}
}
}
2026-03-31 20:00:13 +00:00
#[ derive(Debug, Clone) ]
struct ResumeCommandOutcome {
session : Session ,
message : Option < String > ,
}
2026-03-31 20:22:59 +00:00
#[ derive(Debug, Clone) ]
struct StatusContext {
cwd : PathBuf ,
session_path : Option < PathBuf > ,
loaded_config_files : usize ,
discovered_config_files : usize ,
memory_file_count : usize ,
2026-03-31 21:06:51 +00:00
project_root : Option < PathBuf > ,
git_branch : Option < String > ,
2026-03-31 20:22:59 +00:00
}
#[ derive(Debug, Clone, Copy) ]
struct StatusUsage {
message_count : usize ,
turns : u32 ,
latest : TokenUsage ,
cumulative : TokenUsage ,
estimated_tokens : usize ,
}
2026-03-31 20:43:56 +00:00
fn format_model_report ( model : & str , message_count : usize , turns : u32 ) -> String {
format! (
" Model
Current model { model }
Session messages { message_count }
Session turns { turns }
Usage
Inspect current model with / model
Switch models with / model < name > "
)
}
fn format_model_switch_report ( previous : & str , next : & str , message_count : usize ) -> String {
format! (
" Model updated
Previous { previous }
Current { next }
Preserved msgs { message_count } "
)
}
2026-03-31 21:01:21 +00:00
fn format_permissions_report ( mode : & str ) -> String {
2026-03-31 22:19:58 +00:00
let modes = [
( " read-only " , " Read/search tools only " , mode = = " read-only " ) ,
(
" workspace-write " ,
" Edit files inside the workspace " ,
mode = = " workspace-write " ,
) ,
(
" danger-full-access " ,
" Unrestricted tool access " ,
mode = = " danger-full-access " ,
) ,
]
. into_iter ( )
. map ( | ( name , description , is_current ) | {
let marker = if is_current {
" ● current "
} else {
" ○ available "
} ;
format! ( " {name:<18} {marker:<11} {description} " )
} )
. collect ::< Vec < _ > > ( )
. join (
"
" ,
) ;
2026-03-31 21:01:21 +00:00
format! (
" Permissions
2026-03-31 22:19:58 +00:00
Active mode { mode }
Mode status live session default
2026-03-31 21:01:21 +00:00
2026-03-31 22:19:58 +00:00
Modes
{ modes }
Usage
Inspect current mode with / permissions
Switch modes with / permissions < mode > "
2026-03-31 21:01:21 +00:00
)
}
fn format_permissions_switch_report ( previous : & str , next : & str ) -> String {
format! (
" Permissions updated
2026-03-31 22:19:58 +00:00
Result mode switched
Previous mode { previous }
Active mode { next }
Applies to subsequent tool calls
Usage / permissions to inspect current mode "
2026-03-31 21:01:21 +00:00
)
}
2026-03-31 21:02:24 +00:00
fn format_cost_report ( usage : TokenUsage ) -> String {
format! (
" Cost
Input tokens { }
Output tokens { }
Cache create { }
Cache read { }
Total tokens { } " ,
usage . input_tokens ,
usage . output_tokens ,
usage . cache_creation_input_tokens ,
usage . cache_read_input_tokens ,
usage . total_tokens ( ) ,
)
}
2026-03-31 21:04:42 +00:00
fn format_resume_report ( session_path : & str , message_count : usize , turns : u32 ) -> String {
format! (
" Session resumed
Session file { session_path }
Messages { message_count }
Turns { turns } "
)
}
2026-03-31 21:13:27 +00:00
fn format_init_report ( path : & Path , created : bool ) -> String {
if created {
format! (
" Init
CLAUDE . md { }
Result created
Next step Review and tailor the generated guidance " ,
path . display ( )
)
} else {
format! (
" Init
CLAUDE . md { }
Result skipped ( already exists )
Next step Edit the existing file intentionally if workflows changed " ,
path . display ( )
)
}
}
2026-03-31 21:15:37 +00:00
fn format_compact_report ( removed : usize , resulting_messages : usize , skipped : bool ) -> String {
if skipped {
format! (
" Compact
Result skipped
Reason session below compaction threshold
Messages kept { resulting_messages } "
)
} else {
format! (
" Compact
Result compacted
Messages removed { removed }
Messages kept { resulting_messages } "
)
}
}
2026-03-31 21:06:51 +00:00
fn parse_git_status_metadata ( status : Option < & str > ) -> ( Option < PathBuf > , Option < String > ) {
let Some ( status ) = status else {
return ( None , None ) ;
} ;
let branch = status . lines ( ) . next ( ) . and_then ( | line | {
line . strip_prefix ( " ## " )
. map ( | line | {
line . split ( [ '.' , ' ' ] )
. next ( )
. unwrap_or_default ( )
. to_string ( )
} )
. filter ( | value | ! value . is_empty ( ) )
} ) ;
let project_root = find_git_root ( ) . ok ( ) ;
( project_root , branch )
}
fn find_git_root ( ) -> Result < PathBuf , Box < dyn std ::error ::Error > > {
let output = std ::process ::Command ::new ( " git " )
. args ( [ " rev-parse " , " --show-toplevel " ] )
. current_dir ( env ::current_dir ( ) ? )
. output ( ) ? ;
if ! output . status . success ( ) {
return Err ( " not a git repository " . into ( ) ) ;
}
let path = String ::from_utf8 ( output . stdout ) ? . trim ( ) . to_string ( ) ;
if path . is_empty ( ) {
return Err ( " empty git root " . into ( ) ) ;
}
Ok ( PathBuf ::from ( path ) )
}
2026-03-31 22:49:50 +00:00
#[ allow(clippy::too_many_lines) ]
2026-03-31 19:54:09 +00:00
fn run_resume_command (
session_path : & Path ,
session : & Session ,
command : & SlashCommand ,
2026-03-31 20:00:13 +00:00
) -> Result < ResumeCommandOutcome , Box < dyn std ::error ::Error > > {
2026-03-31 19:54:09 +00:00
match command {
2026-03-31 20:00:13 +00:00
SlashCommand ::Help = > Ok ( ResumeCommandOutcome {
session : session . clone ( ) ,
message : Some ( render_repl_help ( ) ) ,
} ) ,
2026-03-31 19:54:09 +00:00
SlashCommand ::Compact = > {
2026-03-31 21:15:37 +00:00
let result = runtime ::compact_session (
2026-03-31 19:54:09 +00:00
session ,
CompactionConfig {
max_estimated_tokens : 0 ,
.. CompactionConfig ::default ( )
} ,
2026-03-31 21:15:37 +00:00
) ;
let removed = result . removed_message_count ;
let kept = result . compacted_session . messages . len ( ) ;
let skipped = removed = = 0 ;
result . compacted_session . save_to_path ( session_path ) ? ;
2026-03-31 20:00:13 +00:00
Ok ( ResumeCommandOutcome {
2026-03-31 21:15:37 +00:00
session : result . compacted_session ,
message : Some ( format_compact_report ( removed , kept , skipped ) ) ,
2026-03-31 20:00:13 +00:00
} )
}
2026-03-31 20:42:50 +00:00
SlashCommand ::Clear { confirm } = > {
if ! confirm {
return Ok ( ResumeCommandOutcome {
session : session . clone ( ) ,
message : Some (
" clear: confirmation required; rerun with /clear --confirm " . to_string ( ) ,
) ,
} ) ;
}
2026-03-31 20:00:13 +00:00
let cleared = Session ::new ( ) ;
cleared . save_to_path ( session_path ) ? ;
Ok ( ResumeCommandOutcome {
session : cleared ,
message : Some ( format! (
" Cleared resumed session file {}. " ,
session_path . display ( )
) ) ,
} )
2026-03-31 19:54:09 +00:00
}
SlashCommand ::Status = > {
2026-03-31 20:00:13 +00:00
let tracker = UsageTracker ::from_session ( session ) ;
let usage = tracker . cumulative_usage ( ) ;
Ok ( ResumeCommandOutcome {
session : session . clone ( ) ,
2026-03-31 20:22:59 +00:00
message : Some ( format_status_report (
2026-03-31 20:00:13 +00:00
" restored-session " ,
2026-03-31 20:22:59 +00:00
StatusUsage {
message_count : session . messages . len ( ) ,
turns : tracker . turns ( ) ,
latest : tracker . current_turn_usage ( ) ,
cumulative : usage ,
estimated_tokens : 0 ,
} ,
2026-03-31 20:00:13 +00:00
permission_mode_label ( ) ,
2026-03-31 20:22:59 +00:00
& status_context ( Some ( session_path ) ) ? ,
2026-03-31 20:00:13 +00:00
) ) ,
} )
2026-03-31 19:54:09 +00:00
}
SlashCommand ::Cost = > {
let usage = UsageTracker ::from_session ( session ) . cumulative_usage ( ) ;
2026-03-31 20:00:13 +00:00
Ok ( ResumeCommandOutcome {
session : session . clone ( ) ,
2026-03-31 21:02:24 +00:00
message : Some ( format_cost_report ( usage ) ) ,
2026-03-31 20:00:13 +00:00
} )
2026-03-31 19:54:09 +00:00
}
2026-03-31 21:11:57 +00:00
SlashCommand ::Config { section } = > Ok ( ResumeCommandOutcome {
2026-03-31 20:00:13 +00:00
session : session . clone ( ) ,
2026-03-31 21:11:57 +00:00
message : Some ( render_config_report ( section . as_deref ( ) ) ? ) ,
2026-03-31 20:00:13 +00:00
} ) ,
SlashCommand ::Memory = > Ok ( ResumeCommandOutcome {
session : session . clone ( ) ,
message : Some ( render_memory_report ( ) ? ) ,
} ) ,
SlashCommand ::Init = > Ok ( ResumeCommandOutcome {
session : session . clone ( ) ,
message : Some ( init_claude_md ( ) ? ) ,
} ) ,
2026-03-31 22:49:50 +00:00
SlashCommand ::Diff = > Ok ( ResumeCommandOutcome {
session : session . clone ( ) ,
message : Some ( render_diff_report ( ) ? ) ,
} ) ,
SlashCommand ::Version = > Ok ( ResumeCommandOutcome {
session : session . clone ( ) ,
message : Some ( render_version_report ( ) ) ,
} ) ,
SlashCommand ::Export { path } = > {
let export_path = resolve_export_path ( path . as_deref ( ) , session ) ? ;
fs ::write ( & export_path , render_export_text ( session ) ) ? ;
Ok ( ResumeCommandOutcome {
session : session . clone ( ) ,
message : Some ( format! (
" Export \n Result wrote transcript \n File {} \n Messages {} " ,
export_path . display ( ) ,
session . messages . len ( ) ,
) ) ,
} )
}
2026-03-31 19:54:09 +00:00
SlashCommand ::Resume { .. }
| SlashCommand ::Model { .. }
| SlashCommand ::Permissions { .. }
2026-03-31 22:49:50 +00:00
| SlashCommand ::Session { .. }
2026-03-31 19:54:09 +00:00
| SlashCommand ::Unknown ( _ ) = > Err ( " unsupported resumed slash command " . into ( ) ) ,
}
}
2026-03-31 23:38:53 +00:00
fn run_repl (
model : String ,
allowed_tools : Option < AllowedToolSet > ,
) -> Result < ( ) , Box < dyn std ::error ::Error > > {
let mut cli = LiveCli ::new ( model , true , allowed_tools ) ? ;
2026-03-31 18:39:39 +00:00
let editor = input ::LineEditor ::new ( " › " ) ;
2026-03-31 22:49:50 +00:00
println! ( " {} " , cli . startup_banner ( ) ) ;
2026-03-31 18:39:39 +00:00
while let Some ( input ) = editor . read_line ( ) ? {
let trimmed = input . trim ( ) ;
if trimmed . is_empty ( ) {
continue ;
}
2026-03-31 19:23:05 +00:00
if matches! ( trimmed , " /exit " | " /quit " ) {
break ;
}
if let Some ( command ) = SlashCommand ::parse ( trimmed ) {
cli . handle_repl_command ( command ) ? ;
continue ;
2026-03-31 18:39:39 +00:00
}
2026-03-31 19:23:05 +00:00
cli . run_turn ( trimmed ) ? ;
2026-03-31 18:39:39 +00:00
}
Ok ( ( ) )
}
2026-03-31 22:49:50 +00:00
#[ derive(Debug, Clone) ]
struct SessionHandle {
id : String ,
path : PathBuf ,
}
#[ derive(Debug, Clone) ]
struct ManagedSessionSummary {
id : String ,
path : PathBuf ,
modified_epoch_secs : u64 ,
message_count : usize ,
}
2026-03-31 18:39:39 +00:00
struct LiveCli {
model : String ,
2026-03-31 23:38:53 +00:00
allowed_tools : Option < AllowedToolSet > ,
2026-03-31 18:39:39 +00:00
system_prompt : Vec < String > ,
runtime : ConversationRuntime < AnthropicRuntimeClient , CliToolExecutor > ,
2026-03-31 22:49:50 +00:00
session : SessionHandle ,
2026-03-31 18:39:39 +00:00
}
impl LiveCli {
2026-03-31 23:38:53 +00:00
fn new (
model : String ,
enable_tools : bool ,
allowed_tools : Option < AllowedToolSet > ,
) -> Result < Self , Box < dyn std ::error ::Error > > {
2026-03-31 18:39:39 +00:00
let system_prompt = build_system_prompt ( ) ? ;
2026-03-31 22:49:50 +00:00
let session = create_managed_session_handle ( ) ? ;
2026-03-31 18:39:39 +00:00
let runtime = build_runtime (
Session ::new ( ) ,
model . clone ( ) ,
system_prompt . clone ( ) ,
enable_tools ,
2026-03-31 23:38:53 +00:00
allowed_tools . clone ( ) ,
2026-03-31 18:39:39 +00:00
) ? ;
2026-03-31 22:49:50 +00:00
let cli = Self {
2026-03-31 18:39:39 +00:00
model ,
2026-03-31 23:38:53 +00:00
allowed_tools ,
2026-03-31 18:39:39 +00:00
system_prompt ,
runtime ,
2026-03-31 22:49:50 +00:00
session ,
} ;
cli . persist_session ( ) ? ;
Ok ( cli )
}
fn startup_banner ( & self ) -> String {
format! (
" Rusty Claude CLI \n Model {} \n Working directory {} \n Session {} \n \n Type /help for commands. Shift+Enter or Ctrl+J inserts a newline. " ,
self . model ,
env ::current_dir ( ) . map_or_else (
| _ | " <unknown> " . to_string ( ) ,
| path | path . display ( ) . to_string ( ) ,
) ,
self . session . id ,
)
2026-03-31 18:39:39 +00:00
}
fn run_turn ( & mut self , input : & str ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
let mut spinner = Spinner ::new ( ) ;
let mut stdout = io ::stdout ( ) ;
spinner . tick (
" Waiting for Claude " ,
TerminalRenderer ::new ( ) . color_theme ( ) ,
& mut stdout ,
) ? ;
let result = self . runtime . run_turn ( input , None ) ;
match result {
Ok ( _ ) = > {
spinner . finish (
" Claude response complete " ,
TerminalRenderer ::new ( ) . color_theme ( ) ,
& mut stdout ,
) ? ;
println! ( ) ;
2026-03-31 22:49:50 +00:00
self . persist_session ( ) ? ;
2026-03-31 18:39:39 +00:00
Ok ( ( ) )
}
Err ( error ) = > {
spinner . fail (
" Claude request failed " ,
TerminalRenderer ::new ( ) . color_theme ( ) ,
& mut stdout ,
) ? ;
Err ( Box ::new ( error ) )
}
}
}
2026-03-31 22:49:50 +00:00
fn run_turn_with_output (
& mut self ,
input : & str ,
output_format : CliOutputFormat ,
) -> Result < ( ) , Box < dyn std ::error ::Error > > {
match output_format {
CliOutputFormat ::Text = > self . run_turn ( input ) ,
CliOutputFormat ::Json = > self . run_prompt_json ( input ) ,
}
}
fn run_prompt_json ( & mut self , input : & str ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
2026-03-31 23:38:05 +00:00
let client = AnthropicClient ::from_auth ( resolve_cli_auth_source ( ) ? ) ;
2026-03-31 22:49:50 +00:00
let request = MessageRequest {
model : self . model . clone ( ) ,
max_tokens : DEFAULT_MAX_TOKENS ,
messages : vec ! [ InputMessage {
role : " user " . to_string ( ) ,
content : vec ! [ InputContentBlock ::Text {
text : input . to_string ( ) ,
} ] ,
} ] ,
system : ( ! self . system_prompt . is_empty ( ) ) . then ( | | self . system_prompt . join ( " \n \n " ) ) ,
tools : None ,
tool_choice : None ,
stream : false ,
} ;
let runtime = tokio ::runtime ::Runtime ::new ( ) ? ;
let response = runtime . block_on ( client . send_message ( & request ) ) ? ;
let text = response
. content
. iter ( )
. filter_map ( | block | match block {
OutputContentBlock ::Text { text } = > Some ( text . as_str ( ) ) ,
OutputContentBlock ::ToolUse { .. } = > None ,
} )
. collect ::< Vec < _ > > ( )
. join ( " " ) ;
println! (
" {} " ,
json! ( {
" message " : text ,
" model " : self . model ,
" usage " : {
" input_tokens " : response . usage . input_tokens ,
" output_tokens " : response . usage . output_tokens ,
" cache_creation_input_tokens " : response . usage . cache_creation_input_tokens ,
" cache_read_input_tokens " : response . usage . cache_read_input_tokens ,
}
} )
) ;
Ok ( ( ) )
}
2026-03-31 19:23:05 +00:00
fn handle_repl_command (
& mut self ,
command : SlashCommand ,
) -> Result < ( ) , Box < dyn std ::error ::Error > > {
match command {
SlashCommand ::Help = > println! ( " {} " , render_repl_help ( ) ) ,
SlashCommand ::Status = > self . print_status ( ) ,
SlashCommand ::Compact = > self . compact ( ) ? ,
SlashCommand ::Model { model } = > self . set_model ( model ) ? ,
2026-03-31 19:27:31 +00:00
SlashCommand ::Permissions { mode } = > self . set_permissions ( mode ) ? ,
2026-03-31 20:42:50 +00:00
SlashCommand ::Clear { confirm } = > self . clear_session ( confirm ) ? ,
2026-03-31 19:27:31 +00:00
SlashCommand ::Cost = > self . print_cost ( ) ,
2026-03-31 19:45:25 +00:00
SlashCommand ::Resume { session_path } = > self . resume_session ( session_path ) ? ,
2026-03-31 21:11:57 +00:00
SlashCommand ::Config { section } = > Self ::print_config ( section . as_deref ( ) ) ? ,
2026-03-31 19:54:09 +00:00
SlashCommand ::Memory = > Self ::print_memory ( ) ? ,
2026-03-31 19:57:38 +00:00
SlashCommand ::Init = > Self ::run_init ( ) ? ,
2026-03-31 22:49:50 +00:00
SlashCommand ::Diff = > Self ::print_diff ( ) ? ,
SlashCommand ::Version = > Self ::print_version ( ) ,
SlashCommand ::Export { path } = > self . export_session ( path . as_deref ( ) ) ? ,
SlashCommand ::Session { action , target } = > {
self . handle_session_command ( action . as_deref ( ) , target . as_deref ( ) ) ? ;
}
2026-03-31 19:23:05 +00:00
SlashCommand ::Unknown ( name ) = > eprintln! ( " unknown slash command: / {name} " ) ,
}
Ok ( ( ) )
}
2026-03-31 22:49:50 +00:00
fn persist_session ( & self ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
self . runtime . session ( ) . save_to_path ( & self . session . path ) ? ;
Ok ( ( ) )
}
2026-03-31 18:39:39 +00:00
fn print_status ( & self ) {
2026-03-31 19:23:05 +00:00
let cumulative = self . runtime . usage ( ) . cumulative_usage ( ) ;
let latest = self . runtime . usage ( ) . current_turn_usage ( ) ;
2026-03-31 18:39:39 +00:00
println! (
2026-03-31 19:23:05 +00:00
" {} " ,
2026-03-31 20:22:59 +00:00
format_status_report (
2026-03-31 19:23:05 +00:00
& self . model ,
2026-03-31 20:22:59 +00:00
StatusUsage {
message_count : self . runtime . session ( ) . messages . len ( ) ,
turns : self . runtime . usage ( ) . turns ( ) ,
latest ,
cumulative ,
estimated_tokens : self . runtime . estimated_tokens ( ) ,
} ,
2026-03-31 19:23:05 +00:00
permission_mode_label ( ) ,
2026-03-31 22:49:50 +00:00
& status_context ( Some ( & self . session . path ) ) . expect ( " status context should load " ) ,
2026-03-31 19:23:05 +00:00
)
2026-03-31 18:39:39 +00:00
) ;
}
2026-03-31 19:23:05 +00:00
fn set_model ( & mut self , model : Option < String > ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
let Some ( model ) = model else {
2026-03-31 20:43:56 +00:00
println! (
" {} " ,
format_model_report (
& self . model ,
self . runtime . session ( ) . messages . len ( ) ,
self . runtime . usage ( ) . turns ( ) ,
)
) ;
2026-03-31 19:23:05 +00:00
return Ok ( ( ) ) ;
} ;
if model = = self . model {
2026-03-31 20:43:56 +00:00
println! (
" {} " ,
format_model_report (
& self . model ,
self . runtime . session ( ) . messages . len ( ) ,
self . runtime . usage ( ) . turns ( ) ,
)
) ;
2026-03-31 19:23:05 +00:00
return Ok ( ( ) ) ;
}
2026-03-31 20:43:56 +00:00
let previous = self . model . clone ( ) ;
2026-03-31 19:23:05 +00:00
let session = self . runtime . session ( ) . clone ( ) ;
2026-03-31 20:43:56 +00:00
let message_count = session . messages . len ( ) ;
2026-03-31 23:38:53 +00:00
self . runtime = build_runtime (
session ,
model . clone ( ) ,
self . system_prompt . clone ( ) ,
true ,
self . allowed_tools . clone ( ) ,
) ? ;
2026-03-31 19:23:05 +00:00
self . model . clone_from ( & model ) ;
2026-03-31 22:49:50 +00:00
self . persist_session ( ) ? ;
2026-03-31 20:43:56 +00:00
println! (
" {} " ,
format_model_switch_report ( & previous , & model , message_count )
) ;
2026-03-31 19:23:05 +00:00
Ok ( ( ) )
}
2026-03-31 19:27:31 +00:00
fn set_permissions ( & mut self , mode : Option < String > ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
let Some ( mode ) = mode else {
2026-03-31 21:01:21 +00:00
println! ( " {} " , format_permissions_report ( permission_mode_label ( ) ) ) ;
2026-03-31 19:27:31 +00:00
return Ok ( ( ) ) ;
} ;
let normalized = normalize_permission_mode ( & mode ) . ok_or_else ( | | {
format! (
2026-03-31 22:49:50 +00:00
" unsupported permission mode '{mode}'. Use read-only, workspace-write, or danger-full-access. "
2026-03-31 19:27:31 +00:00
)
} ) ? ;
if normalized = = permission_mode_label ( ) {
2026-03-31 21:01:21 +00:00
println! ( " {} " , format_permissions_report ( normalized ) ) ;
2026-03-31 19:27:31 +00:00
return Ok ( ( ) ) ;
}
2026-03-31 21:01:21 +00:00
let previous = permission_mode_label ( ) . to_string ( ) ;
2026-03-31 19:27:31 +00:00
let session = self . runtime . session ( ) . clone ( ) ;
self . runtime = build_runtime_with_permission_mode (
session ,
self . model . clone ( ) ,
self . system_prompt . clone ( ) ,
true ,
2026-03-31 23:38:53 +00:00
self . allowed_tools . clone ( ) ,
2026-03-31 19:27:31 +00:00
normalized ,
) ? ;
2026-03-31 22:49:50 +00:00
self . persist_session ( ) ? ;
2026-03-31 21:01:21 +00:00
println! (
" {} " ,
format_permissions_switch_report ( & previous , normalized )
) ;
2026-03-31 19:27:31 +00:00
Ok ( ( ) )
}
2026-03-31 20:42:50 +00:00
fn clear_session ( & mut self , confirm : bool ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
if ! confirm {
println! (
" clear: confirmation required; run /clear --confirm to start a fresh session. "
) ;
return Ok ( ( ) ) ;
}
2026-03-31 22:49:50 +00:00
self . session = create_managed_session_handle ( ) ? ;
2026-03-31 19:27:31 +00:00
self . runtime = build_runtime_with_permission_mode (
Session ::new ( ) ,
self . model . clone ( ) ,
self . system_prompt . clone ( ) ,
true ,
2026-03-31 23:38:53 +00:00
self . allowed_tools . clone ( ) ,
2026-03-31 19:27:31 +00:00
permission_mode_label ( ) ,
) ? ;
2026-03-31 22:49:50 +00:00
self . persist_session ( ) ? ;
2026-03-31 21:03:49 +00:00
println! (
2026-03-31 22:49:50 +00:00
" Session cleared \n Mode fresh session \n Preserved model {} \n Permission mode {} \n Session {} " ,
2026-03-31 21:03:49 +00:00
self . model ,
2026-03-31 22:49:50 +00:00
permission_mode_label ( ) ,
self . session . id ,
2026-03-31 21:03:49 +00:00
) ;
2026-03-31 19:27:31 +00:00
Ok ( ( ) )
}
fn print_cost ( & self ) {
let cumulative = self . runtime . usage ( ) . cumulative_usage ( ) ;
2026-03-31 21:02:24 +00:00
println! ( " {} " , format_cost_report ( cumulative ) ) ;
2026-03-31 19:27:31 +00:00
}
2026-03-31 19:45:25 +00:00
fn resume_session (
& mut self ,
session_path : Option < String > ,
) -> Result < ( ) , Box < dyn std ::error ::Error > > {
2026-03-31 22:49:50 +00:00
let Some ( session_ref ) = session_path else {
2026-03-31 19:45:25 +00:00
println! ( " Usage: /resume <session-path> " ) ;
return Ok ( ( ) ) ;
} ;
2026-03-31 22:49:50 +00:00
let handle = resolve_session_reference ( & session_ref ) ? ;
let session = Session ::load_from_path ( & handle . path ) ? ;
2026-03-31 19:45:25 +00:00
let message_count = session . messages . len ( ) ;
self . runtime = build_runtime_with_permission_mode (
session ,
self . model . clone ( ) ,
self . system_prompt . clone ( ) ,
true ,
2026-03-31 23:38:53 +00:00
self . allowed_tools . clone ( ) ,
2026-03-31 19:45:25 +00:00
permission_mode_label ( ) ,
) ? ;
2026-03-31 22:49:50 +00:00
self . session = handle ;
self . persist_session ( ) ? ;
2026-03-31 21:04:42 +00:00
println! (
" {} " ,
2026-03-31 22:49:50 +00:00
format_resume_report (
& self . session . path . display ( ) . to_string ( ) ,
message_count ,
self . runtime . usage ( ) . turns ( ) ,
)
2026-03-31 21:04:42 +00:00
) ;
2026-03-31 19:45:25 +00:00
Ok ( ( ) )
}
2026-03-31 21:11:57 +00:00
fn print_config ( section : Option < & str > ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
println! ( " {} " , render_config_report ( section ) ? ) ;
2026-03-31 19:54:09 +00:00
Ok ( ( ) )
}
2026-03-31 19:45:25 +00:00
2026-03-31 19:54:09 +00:00
fn print_memory ( ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
println! ( " {} " , render_memory_report ( ) ? ) ;
2026-03-31 19:45:25 +00:00
Ok ( ( ) )
}
2026-03-31 19:57:38 +00:00
fn run_init ( ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
println! ( " {} " , init_claude_md ( ) ? ) ;
Ok ( ( ) )
}
2026-03-31 22:49:50 +00:00
fn print_diff ( ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
println! ( " {} " , render_diff_report ( ) ? ) ;
Ok ( ( ) )
}
fn print_version ( ) {
println! ( " {} " , render_version_report ( ) ) ;
}
fn export_session (
& self ,
requested_path : Option < & str > ,
) -> Result < ( ) , Box < dyn std ::error ::Error > > {
let export_path = resolve_export_path ( requested_path , self . runtime . session ( ) ) ? ;
fs ::write ( & export_path , render_export_text ( self . runtime . session ( ) ) ) ? ;
println! (
" Export \n Result wrote transcript \n File {} \n Messages {} " ,
export_path . display ( ) ,
self . runtime . session ( ) . messages . len ( ) ,
) ;
Ok ( ( ) )
}
fn handle_session_command (
& mut self ,
action : Option < & str > ,
target : Option < & str > ,
) -> Result < ( ) , Box < dyn std ::error ::Error > > {
match action {
None | Some ( " list " ) = > {
println! ( " {} " , render_session_list ( & self . session . id ) ? ) ;
Ok ( ( ) )
}
Some ( " switch " ) = > {
let Some ( target ) = target else {
println! ( " Usage: /session switch <session-id> " ) ;
return Ok ( ( ) ) ;
} ;
let handle = resolve_session_reference ( target ) ? ;
let session = Session ::load_from_path ( & handle . path ) ? ;
let message_count = session . messages . len ( ) ;
self . runtime = build_runtime_with_permission_mode (
session ,
self . model . clone ( ) ,
self . system_prompt . clone ( ) ,
true ,
2026-03-31 23:38:53 +00:00
self . allowed_tools . clone ( ) ,
2026-03-31 22:49:50 +00:00
permission_mode_label ( ) ,
) ? ;
self . session = handle ;
self . persist_session ( ) ? ;
println! (
" Session switched \n Active session {} \n File {} \n Messages {} " ,
self . session . id ,
self . session . path . display ( ) ,
message_count ,
) ;
Ok ( ( ) )
}
Some ( other ) = > {
println! ( " Unknown /session action ' {other} '. Use /session list or /session switch <session-id>. " ) ;
Ok ( ( ) )
}
}
}
2026-03-31 18:39:39 +00:00
fn compact ( & mut self ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
let result = self . runtime . compact ( CompactionConfig ::default ( ) ) ;
let removed = result . removed_message_count ;
2026-03-31 21:15:37 +00:00
let kept = result . compacted_session . messages . len ( ) ;
let skipped = removed = = 0 ;
2026-03-31 19:27:31 +00:00
self . runtime = build_runtime_with_permission_mode (
2026-03-31 18:39:39 +00:00
result . compacted_session ,
self . model . clone ( ) ,
self . system_prompt . clone ( ) ,
true ,
2026-03-31 23:38:53 +00:00
self . allowed_tools . clone ( ) ,
2026-03-31 19:27:31 +00:00
permission_mode_label ( ) ,
2026-03-31 18:39:39 +00:00
) ? ;
2026-03-31 22:49:50 +00:00
self . persist_session ( ) ? ;
2026-03-31 21:15:37 +00:00
println! ( " {} " , format_compact_report ( removed , kept , skipped ) ) ;
2026-03-31 18:39:39 +00:00
Ok ( ( ) )
}
}
2026-03-31 22:49:50 +00:00
fn sessions_dir ( ) -> Result < PathBuf , Box < dyn std ::error ::Error > > {
let cwd = env ::current_dir ( ) ? ;
let path = cwd . join ( " .claude " ) . join ( " sessions " ) ;
fs ::create_dir_all ( & path ) ? ;
Ok ( path )
}
fn create_managed_session_handle ( ) -> Result < SessionHandle , Box < dyn std ::error ::Error > > {
let id = generate_session_id ( ) ;
let path = sessions_dir ( ) ? . join ( format! ( " {id} .json " ) ) ;
Ok ( SessionHandle { id , path } )
}
fn generate_session_id ( ) -> String {
let millis = SystemTime ::now ( )
. duration_since ( UNIX_EPOCH )
. map ( | duration | duration . as_millis ( ) )
. unwrap_or_default ( ) ;
format! ( " session- {millis} " )
}
fn resolve_session_reference ( reference : & str ) -> Result < SessionHandle , Box < dyn std ::error ::Error > > {
let direct = PathBuf ::from ( reference ) ;
let path = if direct . exists ( ) {
direct
} else {
sessions_dir ( ) ? . join ( format! ( " {reference} .json " ) )
} ;
if ! path . exists ( ) {
return Err ( format! ( " session not found: {reference} " ) . into ( ) ) ;
}
let id = path
. file_stem ( )
. and_then ( | value | value . to_str ( ) )
. unwrap_or ( reference )
. to_string ( ) ;
Ok ( SessionHandle { id , path } )
}
fn list_managed_sessions ( ) -> Result < Vec < ManagedSessionSummary > , Box < dyn std ::error ::Error > > {
let mut sessions = Vec ::new ( ) ;
for entry in fs ::read_dir ( sessions_dir ( ) ? ) ? {
let entry = entry ? ;
let path = entry . path ( ) ;
if path . extension ( ) . and_then ( | ext | ext . to_str ( ) ) ! = Some ( " json " ) {
continue ;
}
let metadata = entry . metadata ( ) ? ;
let modified_epoch_secs = metadata
. modified ( )
. ok ( )
. and_then ( | time | time . duration_since ( UNIX_EPOCH ) . ok ( ) )
. map ( | duration | duration . as_secs ( ) )
. unwrap_or_default ( ) ;
let message_count = Session ::load_from_path ( & path )
. map ( | session | session . messages . len ( ) )
. unwrap_or_default ( ) ;
let id = path
. file_stem ( )
. and_then ( | value | value . to_str ( ) )
. unwrap_or ( " unknown " )
. to_string ( ) ;
sessions . push ( ManagedSessionSummary {
id ,
path ,
modified_epoch_secs ,
message_count ,
} ) ;
}
sessions . sort_by ( | left , right | right . modified_epoch_secs . cmp ( & left . modified_epoch_secs ) ) ;
Ok ( sessions )
}
fn render_session_list ( active_session_id : & str ) -> Result < String , Box < dyn std ::error ::Error > > {
let sessions = list_managed_sessions ( ) ? ;
let mut lines = vec! [
" Sessions " . to_string ( ) ,
format! ( " Directory {} " , sessions_dir ( ) ? . display ( ) ) ,
] ;
if sessions . is_empty ( ) {
lines . push ( " No managed sessions saved yet. " . to_string ( ) ) ;
return Ok ( lines . join ( " \n " ) ) ;
}
for session in sessions {
let marker = if session . id = = active_session_id {
" ● current "
} else {
" ○ saved "
} ;
lines . push ( format! (
" {id:<20} {marker:<10} msgs={msgs:<4} modified={modified} path={path} " ,
id = session . id ,
msgs = session . message_count ,
modified = session . modified_epoch_secs ,
path = session . path . display ( ) ,
) ) ;
}
Ok ( lines . join ( " \n " ) )
}
2026-03-31 19:23:05 +00:00
fn render_repl_help ( ) -> String {
2026-03-31 21:03:49 +00:00
[
" REPL " . to_string ( ) ,
" /exit Quit the REPL " . to_string ( ) ,
" /quit Quit the REPL " . to_string ( ) ,
String ::new ( ) ,
render_slash_command_help ( ) ,
]
. join (
"
" ,
2026-03-31 19:23:05 +00:00
)
}
2026-03-31 20:22:59 +00:00
fn status_context (
session_path : Option < & Path > ,
) -> Result < StatusContext , Box < dyn std ::error ::Error > > {
let cwd = env ::current_dir ( ) ? ;
let loader = ConfigLoader ::default_for ( & cwd ) ;
let discovered_config_files = loader . discover ( ) . len ( ) ;
let runtime_config = loader . load ( ) ? ;
2026-03-31 21:06:51 +00:00
let project_context = ProjectContext ::discover_with_git ( & cwd , DEFAULT_DATE ) ? ;
let ( project_root , git_branch ) =
parse_git_status_metadata ( project_context . git_status . as_deref ( ) ) ;
2026-03-31 20:22:59 +00:00
Ok ( StatusContext {
cwd ,
session_path : session_path . map ( Path ::to_path_buf ) ,
loaded_config_files : runtime_config . loaded_entries ( ) . len ( ) ,
discovered_config_files ,
memory_file_count : project_context . instruction_files . len ( ) ,
2026-03-31 21:06:51 +00:00
project_root ,
git_branch ,
2026-03-31 20:22:59 +00:00
} )
}
fn format_status_report (
2026-03-31 19:23:05 +00:00
model : & str ,
2026-03-31 20:22:59 +00:00
usage : StatusUsage ,
2026-03-31 19:23:05 +00:00
permission_mode : & str ,
2026-03-31 20:22:59 +00:00
context : & StatusContext ,
2026-03-31 19:23:05 +00:00
) -> String {
2026-03-31 20:41:39 +00:00
[
format! (
" Status
Model { model }
Permission mode { permission_mode }
Messages { }
Turns { }
Estimated tokens { } " ,
usage . message_count , usage . turns , usage . estimated_tokens ,
) ,
format! (
" Usage
Latest total { }
Cumulative input { }
Cumulative output { }
Cumulative total { } " ,
usage . latest . total_tokens ( ) ,
usage . cumulative . input_tokens ,
usage . cumulative . output_tokens ,
usage . cumulative . total_tokens ( ) ,
) ,
format! (
" Workspace
Cwd { }
2026-03-31 21:06:51 +00:00
Project root { }
Git branch { }
2026-03-31 20:41:39 +00:00
Session { }
Config files loaded { } / { }
Memory files { } " ,
context . cwd . display ( ) ,
2026-03-31 21:06:51 +00:00
context
. project_root
. as_ref ( )
. map_or_else ( | | " unknown " . to_string ( ) , | path | path . display ( ) . to_string ( ) ) ,
context . git_branch . as_deref ( ) . unwrap_or ( " unknown " ) ,
2026-03-31 20:41:39 +00:00
context . session_path . as_ref ( ) . map_or_else (
| | " live-repl " . to_string ( ) ,
| path | path . display ( ) . to_string ( )
) ,
context . loaded_config_files ,
context . discovered_config_files ,
context . memory_file_count ,
) ,
]
. join (
2026-03-31 20:22:59 +00:00
"
2026-03-31 20:41:39 +00:00
2026-03-31 20:22:59 +00:00
" ,
2026-03-31 19:23:05 +00:00
)
}
2026-03-31 21:11:57 +00:00
fn render_config_report ( section : Option < & str > ) -> Result < String , Box < dyn std ::error ::Error > > {
2026-03-31 19:54:09 +00:00
let cwd = env ::current_dir ( ) ? ;
let loader = ConfigLoader ::default_for ( & cwd ) ;
let discovered = loader . discover ( ) ;
let runtime_config = loader . load ( ) ? ;
2026-03-31 20:41:39 +00:00
let mut lines = vec! [
format! (
" Config
Working directory { }
Loaded files { }
Merged keys { } " ,
cwd . display ( ) ,
runtime_config . loaded_entries ( ) . len ( ) ,
runtime_config . merged ( ) . len ( )
) ,
" Discovered files " . to_string ( ) ,
] ;
2026-03-31 19:54:09 +00:00
for entry in discovered {
let source = match entry . source {
ConfigSource ::User = > " user " ,
ConfigSource ::Project = > " project " ,
ConfigSource ::Local = > " local " ,
} ;
let status = if runtime_config
. loaded_entries ( )
. iter ( )
. any ( | loaded_entry | loaded_entry . path = = entry . path )
{
" loaded "
} else {
" missing "
} ;
lines . push ( format! (
" {source:<7} {status:<7} {} " ,
entry . path . display ( )
) ) ;
}
2026-03-31 21:11:57 +00:00
if let Some ( section ) = section {
lines . push ( format! ( " Merged section: {section} " ) ) ;
let value = match section {
" env " = > runtime_config . get ( " env " ) ,
" hooks " = > runtime_config . get ( " hooks " ) ,
" model " = > runtime_config . get ( " model " ) ,
other = > {
lines . push ( format! (
" Unsupported config section '{other}'. Use env, hooks, or model. "
) ) ;
return Ok ( lines . join (
"
" ,
) ) ;
}
} ;
lines . push ( format! (
" {} " ,
match value {
Some ( value ) = > value . render ( ) ,
None = > " <unset> " . to_string ( ) ,
}
) ) ;
return Ok ( lines . join (
"
" ,
) ) ;
}
2026-03-31 20:41:39 +00:00
lines . push ( " Merged JSON " . to_string ( ) ) ;
lines . push ( format! ( " {} " , runtime_config . as_json ( ) . render ( ) ) ) ;
2026-03-31 19:54:09 +00:00
Ok ( lines . join (
"
" ,
) )
}
fn render_memory_report ( ) -> Result < String , Box < dyn std ::error ::Error > > {
2026-03-31 21:08:19 +00:00
let cwd = env ::current_dir ( ) ? ;
let project_context = ProjectContext ::discover ( & cwd , DEFAULT_DATE ) ? ;
2026-03-31 19:54:09 +00:00
let mut lines = vec! [ format! (
2026-03-31 21:08:19 +00:00
" Memory
Working directory { }
Instruction files { } " ,
cwd . display ( ) ,
2026-03-31 19:54:09 +00:00
project_context . instruction_files . len ( )
) ] ;
if project_context . instruction_files . is_empty ( ) {
2026-03-31 21:08:19 +00:00
lines . push ( " Discovered files " . to_string ( ) ) ;
2026-03-31 19:54:09 +00:00
lines . push (
" No CLAUDE instruction files discovered in the current directory ancestry. "
. to_string ( ) ,
) ;
} else {
2026-03-31 21:08:19 +00:00
lines . push ( " Discovered files " . to_string ( ) ) ;
for ( index , file ) in project_context . instruction_files . iter ( ) . enumerate ( ) {
2026-03-31 19:54:09 +00:00
let preview = file . content . lines ( ) . next ( ) . unwrap_or ( " " ) . trim ( ) ;
let preview = if preview . is_empty ( ) {
" <empty> "
} else {
preview
} ;
2026-03-31 21:08:19 +00:00
lines . push ( format! ( " {} . {} " , index + 1 , file . path . display ( ) , ) ) ;
2026-03-31 19:54:09 +00:00
lines . push ( format! (
2026-03-31 21:08:19 +00:00
" lines={} preview={} " ,
2026-03-31 19:54:09 +00:00
file . content . lines ( ) . count ( ) ,
preview
) ) ;
}
}
Ok ( lines . join (
"
" ,
) )
}
2026-03-31 19:57:38 +00:00
fn init_claude_md ( ) -> Result < String , Box < dyn std ::error ::Error > > {
let cwd = env ::current_dir ( ) ? ;
let claude_md = cwd . join ( " CLAUDE.md " ) ;
if claude_md . exists ( ) {
2026-03-31 21:13:27 +00:00
return Ok ( format_init_report ( & claude_md , false ) ) ;
2026-03-31 19:57:38 +00:00
}
let content = render_init_claude_md ( & cwd ) ;
fs ::write ( & claude_md , content ) ? ;
2026-03-31 21:13:27 +00:00
Ok ( format_init_report ( & claude_md , true ) )
2026-03-31 19:57:38 +00:00
}
fn render_init_claude_md ( cwd : & Path ) -> String {
let mut lines = vec! [
" # CLAUDE.md " . to_string ( ) ,
String ::new ( ) ,
" This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. " . to_string ( ) ,
String ::new ( ) ,
] ;
let mut command_lines = Vec ::new ( ) ;
if cwd . join ( " rust " ) . join ( " Cargo.toml " ) . is_file ( ) {
command_lines . push ( " - Run Rust verification from `rust/`: `cargo fmt`, `cargo clippy --workspace --all-targets -- -D warnings`, `cargo test --workspace` " . to_string ( ) ) ;
} else if cwd . join ( " Cargo.toml " ) . is_file ( ) {
command_lines . push ( " - Run Rust verification from the repo root: `cargo fmt`, `cargo clippy --workspace --all-targets -- -D warnings`, `cargo test --workspace` " . to_string ( ) ) ;
}
if cwd . join ( " tests " ) . is_dir ( ) & & cwd . join ( " src " ) . is_dir ( ) {
command_lines . push ( " - `src/` and `tests/` are also present; check those surfaces before removing or renaming Python-era compatibility assets. " . to_string ( ) ) ;
}
if ! command_lines . is_empty ( ) {
lines . push ( " ## Verification " . to_string ( ) ) ;
lines . extend ( command_lines ) ;
lines . push ( String ::new ( ) ) ;
}
let mut structure_lines = Vec ::new ( ) ;
if cwd . join ( " rust " ) . is_dir ( ) {
structure_lines . push (
" - `rust/` contains the Rust workspace and the active CLI/runtime implementation. "
. to_string ( ) ,
) ;
}
if cwd . join ( " src " ) . is_dir ( ) {
structure_lines . push ( " - `src/` contains the older Python-first workspace artifacts referenced by the repo history and tests. " . to_string ( ) ) ;
}
if cwd . join ( " tests " ) . is_dir ( ) {
structure_lines . push ( " - `tests/` exercises compatibility and porting behavior across the repository surfaces. " . to_string ( ) ) ;
}
if ! structure_lines . is_empty ( ) {
lines . push ( " ## Repository shape " . to_string ( ) ) ;
lines . extend ( structure_lines ) ;
lines . push ( String ::new ( ) ) ;
}
lines . push ( " ## Working agreement " . to_string ( ) ) ;
lines . push ( " - Prefer small, reviewable Rust changes and keep slash-command behavior aligned between the shared command registry and the CLI entrypoints. " . to_string ( ) ) ;
lines . push ( " - Do not overwrite existing CLAUDE.md content automatically; update it intentionally when repo workflows change. " . to_string ( ) ) ;
lines . push ( String ::new ( ) ) ;
lines . join (
"
" ,
)
}
2026-03-31 19:27:31 +00:00
fn normalize_permission_mode ( mode : & str ) -> Option < & 'static str > {
match mode . trim ( ) {
" read-only " = > Some ( " read-only " ) ,
" workspace-write " = > Some ( " workspace-write " ) ,
" danger-full-access " = > Some ( " danger-full-access " ) ,
_ = > None ,
}
}
2026-03-31 19:23:05 +00:00
fn permission_mode_label ( ) -> & 'static str {
match env ::var ( " RUSTY_CLAUDE_PERMISSION_MODE " ) {
Ok ( value ) if value = = " read-only " = > " read-only " ,
2026-03-31 19:27:31 +00:00
Ok ( value ) if value = = " danger-full-access " = > " danger-full-access " ,
2026-03-31 19:23:05 +00:00
_ = > " workspace-write " ,
}
}
2026-03-31 22:49:50 +00:00
fn render_diff_report ( ) -> Result < String , Box < dyn std ::error ::Error > > {
let output = std ::process ::Command ::new ( " git " )
. args ( [ " diff " , " -- " , " :(exclude).omx " ] )
. current_dir ( env ::current_dir ( ) ? )
. output ( ) ? ;
if ! output . status . success ( ) {
let stderr = String ::from_utf8_lossy ( & output . stderr ) . trim ( ) . to_string ( ) ;
return Err ( format! ( " git diff failed: {stderr} " ) . into ( ) ) ;
}
let diff = String ::from_utf8 ( output . stdout ) ? ;
if diff . trim ( ) . is_empty ( ) {
return Ok (
" Diff \n Result clean working tree \n Detail no current changes "
. to_string ( ) ,
) ;
}
Ok ( format! ( " Diff \n \n {} " , diff . trim_end ( ) ) )
}
fn render_version_report ( ) -> String {
let git_sha = GIT_SHA . unwrap_or ( " unknown " ) ;
let target = BUILD_TARGET . unwrap_or ( " unknown " ) ;
format! (
" Version \n Version {VERSION} \n Git SHA {git_sha} \n Target {target} \n Build date {DEFAULT_DATE} "
)
}
fn render_export_text ( session : & Session ) -> String {
let mut lines = vec! [ " # Conversation Export " . to_string ( ) , String ::new ( ) ] ;
for ( index , message ) in session . messages . iter ( ) . enumerate ( ) {
let role = match message . role {
MessageRole ::System = > " system " ,
MessageRole ::User = > " user " ,
MessageRole ::Assistant = > " assistant " ,
MessageRole ::Tool = > " tool " ,
} ;
lines . push ( format! ( " ## {} . {role} " , index + 1 ) ) ;
for block in & message . blocks {
match block {
ContentBlock ::Text { text } = > lines . push ( text . clone ( ) ) ,
ContentBlock ::ToolUse { id , name , input } = > {
lines . push ( format! ( " [tool_use id= {id} name= {name} ] {input} " ) ) ;
}
ContentBlock ::ToolResult {
tool_use_id ,
tool_name ,
output ,
is_error ,
} = > {
lines . push ( format! (
" [tool_result id={tool_use_id} name={tool_name} error={is_error}] {output} "
) ) ;
}
}
}
lines . push ( String ::new ( ) ) ;
}
lines . join ( " \n " )
}
fn default_export_filename ( session : & Session ) -> String {
let stem = session
. messages
. iter ( )
. find_map ( | message | match message . role {
MessageRole ::User = > message . blocks . iter ( ) . find_map ( | block | match block {
ContentBlock ::Text { text } = > Some ( text . as_str ( ) ) ,
_ = > None ,
} ) ,
_ = > None ,
} )
. map_or ( " conversation " , | text | {
text . lines ( ) . next ( ) . unwrap_or ( " conversation " )
} )
. chars ( )
. map ( | ch | {
if ch . is_ascii_alphanumeric ( ) {
ch . to_ascii_lowercase ( )
} else {
'-'
}
} )
. collect ::< String > ( )
. split ( '-' )
. filter ( | part | ! part . is_empty ( ) )
. take ( 8 )
. collect ::< Vec < _ > > ( )
. join ( " - " ) ;
let fallback = if stem . is_empty ( ) {
" conversation "
} else {
& stem
} ;
format! ( " {fallback} .txt " )
}
fn resolve_export_path (
requested_path : Option < & str > ,
session : & Session ,
) -> Result < PathBuf , Box < dyn std ::error ::Error > > {
let cwd = env ::current_dir ( ) ? ;
let file_name =
requested_path . map_or_else ( | | default_export_filename ( session ) , ToOwned ::to_owned ) ;
let final_name = if Path ::new ( & file_name )
. extension ( )
. is_some_and ( | ext | ext . eq_ignore_ascii_case ( " txt " ) )
{
file_name
} else {
format! ( " {file_name} .txt " )
} ;
Ok ( cwd . join ( final_name ) )
}
2026-03-31 18:39:39 +00:00
fn build_system_prompt ( ) -> Result < Vec < String > , Box < dyn std ::error ::Error > > {
Ok ( load_system_prompt (
env ::current_dir ( ) ? ,
DEFAULT_DATE ,
env ::consts ::OS ,
" unknown " ,
) ? )
}
fn build_runtime (
session : Session ,
model : String ,
system_prompt : Vec < String > ,
enable_tools : bool ,
2026-03-31 23:38:53 +00:00
allowed_tools : Option < AllowedToolSet > ,
2026-03-31 18:39:39 +00:00
) -> Result < ConversationRuntime < AnthropicRuntimeClient , CliToolExecutor > , Box < dyn std ::error ::Error > >
2026-03-31 19:27:31 +00:00
{
build_runtime_with_permission_mode (
session ,
model ,
system_prompt ,
enable_tools ,
2026-03-31 23:38:53 +00:00
allowed_tools ,
2026-03-31 19:27:31 +00:00
permission_mode_label ( ) ,
)
}
fn build_runtime_with_permission_mode (
session : Session ,
model : String ,
system_prompt : Vec < String > ,
enable_tools : bool ,
2026-03-31 23:38:53 +00:00
allowed_tools : Option < AllowedToolSet > ,
2026-03-31 19:27:31 +00:00
permission_mode : & str ,
) -> Result < ConversationRuntime < AnthropicRuntimeClient , CliToolExecutor > , Box < dyn std ::error ::Error > >
2026-03-31 18:39:39 +00:00
{
Ok ( ConversationRuntime ::new (
session ,
2026-03-31 23:38:53 +00:00
AnthropicRuntimeClient ::new ( model , enable_tools , allowed_tools . clone ( ) ) ? ,
CliToolExecutor ::new ( allowed_tools ) ,
2026-03-31 19:27:31 +00:00
permission_policy ( permission_mode ) ,
2026-03-31 18:39:39 +00:00
system_prompt ,
) )
}
struct AnthropicRuntimeClient {
runtime : tokio ::runtime ::Runtime ,
client : AnthropicClient ,
model : String ,
enable_tools : bool ,
2026-03-31 23:38:53 +00:00
allowed_tools : Option < AllowedToolSet > ,
2026-03-31 18:39:39 +00:00
}
impl AnthropicRuntimeClient {
2026-03-31 23:38:53 +00:00
fn new (
model : String ,
enable_tools : bool ,
allowed_tools : Option < AllowedToolSet > ,
) -> Result < Self , Box < dyn std ::error ::Error > > {
2026-03-31 18:39:39 +00:00
Ok ( Self {
runtime : tokio ::runtime ::Runtime ::new ( ) ? ,
2026-03-31 23:38:05 +00:00
client : AnthropicClient ::from_auth ( resolve_cli_auth_source ( ) ? ) ,
2026-03-31 18:39:39 +00:00
model ,
enable_tools ,
2026-03-31 23:38:53 +00:00
allowed_tools ,
2026-03-31 18:39:39 +00:00
} )
}
}
2026-03-31 23:38:05 +00:00
fn resolve_cli_auth_source ( ) -> Result < AuthSource , Box < dyn std ::error ::Error > > {
match AuthSource ::from_env ( ) {
Ok ( auth ) = > Ok ( auth ) ,
Err ( api ::ApiError ::MissingApiKey ) = > {
let cwd = env ::current_dir ( ) ? ;
let config = ConfigLoader ::default_for ( & cwd ) . load ( ) ? ;
if let Some ( oauth ) = config . oauth ( ) {
if let Some ( token_set ) = resolve_saved_oauth_token ( oauth ) ? {
return Ok ( AuthSource ::from ( token_set ) ) ;
}
}
Ok ( AuthSource ::from_env_or_saved ( ) ? )
}
Err ( error ) = > Err ( Box ::new ( error ) ) ,
}
}
2026-03-31 18:39:39 +00:00
impl ApiClient for AnthropicRuntimeClient {
2026-03-31 19:23:05 +00:00
#[ allow(clippy::too_many_lines) ]
2026-03-31 18:39:39 +00:00
fn stream ( & mut self , request : ApiRequest ) -> Result < Vec < AssistantEvent > , RuntimeError > {
let message_request = MessageRequest {
model : self . model . clone ( ) ,
max_tokens : DEFAULT_MAX_TOKENS ,
messages : convert_messages ( & request . messages ) ,
system : ( ! request . system_prompt . is_empty ( ) ) . then ( | | request . system_prompt . join ( " \n \n " ) ) ,
tools : self . enable_tools . then ( | | {
2026-03-31 23:38:53 +00:00
filter_tool_specs ( self . allowed_tools . as_ref ( ) )
2026-03-31 18:39:39 +00:00
. into_iter ( )
. map ( | spec | ToolDefinition {
name : spec . name . to_string ( ) ,
description : Some ( spec . description . to_string ( ) ) ,
input_schema : spec . input_schema ,
} )
. collect ( )
} ) ,
tool_choice : self . enable_tools . then_some ( ToolChoice ::Auto ) ,
stream : true ,
} ;
self . runtime . block_on ( async {
let mut stream = self
. client
. stream_message ( & message_request )
. await
. map_err ( | error | RuntimeError ::new ( error . to_string ( ) ) ) ? ;
let mut stdout = io ::stdout ( ) ;
let mut events = Vec ::new ( ) ;
let mut pending_tool : Option < ( String , String , String ) > = None ;
let mut saw_stop = false ;
while let Some ( event ) = stream
. next_event ( )
. await
. map_err ( | error | RuntimeError ::new ( error . to_string ( ) ) ) ?
{
match event {
ApiStreamEvent ::MessageStart ( start ) = > {
for block in start . message . content {
push_output_block ( block , & mut stdout , & mut events , & mut pending_tool ) ? ;
}
}
ApiStreamEvent ::ContentBlockStart ( start ) = > {
push_output_block (
start . content_block ,
& mut stdout ,
& mut events ,
& mut pending_tool ,
) ? ;
}
ApiStreamEvent ::ContentBlockDelta ( delta ) = > match delta . delta {
ContentBlockDelta ::TextDelta { text } = > {
if ! text . is_empty ( ) {
write! ( stdout , " {text} " )
2026-03-31 19:23:05 +00:00
. and_then ( | ( ) | stdout . flush ( ) )
2026-03-31 18:39:39 +00:00
. map_err ( | error | RuntimeError ::new ( error . to_string ( ) ) ) ? ;
events . push ( AssistantEvent ::TextDelta ( text ) ) ;
}
}
ContentBlockDelta ::InputJsonDelta { partial_json } = > {
if let Some ( ( _ , _ , input ) ) = & mut pending_tool {
input . push_str ( & partial_json ) ;
}
}
} ,
ApiStreamEvent ::ContentBlockStop ( _ ) = > {
if let Some ( ( id , name , input ) ) = pending_tool . take ( ) {
events . push ( AssistantEvent ::ToolUse { id , name , input } ) ;
}
}
ApiStreamEvent ::MessageDelta ( delta ) = > {
events . push ( AssistantEvent ::Usage ( TokenUsage {
input_tokens : delta . usage . input_tokens ,
output_tokens : delta . usage . output_tokens ,
cache_creation_input_tokens : 0 ,
cache_read_input_tokens : 0 ,
} ) ) ;
}
ApiStreamEvent ::MessageStop ( _ ) = > {
saw_stop = true ;
events . push ( AssistantEvent ::MessageStop ) ;
}
}
}
if ! saw_stop
& & events . iter ( ) . any ( | event | {
matches! ( event , AssistantEvent ::TextDelta ( text ) if ! text . is_empty ( ) )
| | matches! ( event , AssistantEvent ::ToolUse { .. } )
} )
{
events . push ( AssistantEvent ::MessageStop ) ;
}
if events
. iter ( )
. any ( | event | matches! ( event , AssistantEvent ::MessageStop ) )
{
return Ok ( events ) ;
}
let response = self
. client
. send_message ( & MessageRequest {
stream : false ,
.. message_request . clone ( )
} )
. await
. map_err ( | error | RuntimeError ::new ( error . to_string ( ) ) ) ? ;
response_to_events ( response , & mut stdout )
} )
}
}
fn push_output_block (
block : OutputContentBlock ,
out : & mut impl Write ,
events : & mut Vec < AssistantEvent > ,
pending_tool : & mut Option < ( String , String , String ) > ,
) -> Result < ( ) , RuntimeError > {
match block {
OutputContentBlock ::Text { text } = > {
if ! text . is_empty ( ) {
write! ( out , " {text} " )
2026-03-31 19:23:05 +00:00
. and_then ( | ( ) | out . flush ( ) )
2026-03-31 18:39:39 +00:00
. map_err ( | error | RuntimeError ::new ( error . to_string ( ) ) ) ? ;
events . push ( AssistantEvent ::TextDelta ( text ) ) ;
}
}
OutputContentBlock ::ToolUse { id , name , input } = > {
* pending_tool = Some ( ( id , name , input . to_string ( ) ) ) ;
}
}
Ok ( ( ) )
}
fn response_to_events (
response : MessageResponse ,
out : & mut impl Write ,
) -> Result < Vec < AssistantEvent > , RuntimeError > {
let mut events = Vec ::new ( ) ;
let mut pending_tool = None ;
for block in response . content {
push_output_block ( block , out , & mut events , & mut pending_tool ) ? ;
if let Some ( ( id , name , input ) ) = pending_tool . take ( ) {
events . push ( AssistantEvent ::ToolUse { id , name , input } ) ;
}
}
events . push ( AssistantEvent ::Usage ( TokenUsage {
input_tokens : response . usage . input_tokens ,
output_tokens : response . usage . output_tokens ,
cache_creation_input_tokens : response . usage . cache_creation_input_tokens ,
cache_read_input_tokens : response . usage . cache_read_input_tokens ,
} ) ) ;
events . push ( AssistantEvent ::MessageStop ) ;
Ok ( events )
}
struct CliToolExecutor {
renderer : TerminalRenderer ,
2026-03-31 23:38:53 +00:00
allowed_tools : Option < AllowedToolSet > ,
2026-03-31 18:39:39 +00:00
}
impl CliToolExecutor {
2026-03-31 23:38:53 +00:00
fn new ( allowed_tools : Option < AllowedToolSet > ) -> Self {
2026-03-31 18:39:39 +00:00
Self {
renderer : TerminalRenderer ::new ( ) ,
2026-03-31 23:38:53 +00:00
allowed_tools ,
2026-03-31 18:39:39 +00:00
}
}
}
impl ToolExecutor for CliToolExecutor {
fn execute ( & mut self , tool_name : & str , input : & str ) -> Result < String , ToolError > {
2026-03-31 23:38:53 +00:00
if self
. allowed_tools
. as_ref ( )
. is_some_and ( | allowed | ! allowed . contains ( tool_name ) )
{
return Err ( ToolError ::new ( format! (
" tool `{tool_name}` is not enabled by the current --allowedTools setting "
) ) ) ;
}
2026-03-31 18:39:39 +00:00
let value = serde_json ::from_str ( input )
. map_err ( | error | ToolError ::new ( format! ( " invalid tool input JSON: {error} " ) ) ) ? ;
match execute_tool ( tool_name , & value ) {
Ok ( output ) = > {
let markdown = format! ( " ### Tool ` {tool_name} ` \n \n ```json \n {output} \n ``` \n " ) ;
self . renderer
. stream_markdown ( & markdown , & mut io ::stdout ( ) )
. map_err ( | error | ToolError ::new ( error . to_string ( ) ) ) ? ;
Ok ( output )
}
Err ( error ) = > Err ( ToolError ::new ( error ) ) ,
}
}
}
2026-03-31 19:27:31 +00:00
fn permission_policy ( mode : & str ) -> PermissionPolicy {
if normalize_permission_mode ( mode ) = = Some ( " read-only " ) {
PermissionPolicy ::new ( PermissionMode ::Deny )
2026-03-31 18:39:39 +00:00
. with_tool_mode ( " read_file " , PermissionMode ::Allow )
. with_tool_mode ( " glob_search " , PermissionMode ::Allow )
2026-03-31 19:27:31 +00:00
. with_tool_mode ( " grep_search " , PermissionMode ::Allow )
} else {
PermissionPolicy ::new ( PermissionMode ::Allow )
2026-03-31 18:39:39 +00:00
}
}
fn convert_messages ( messages : & [ ConversationMessage ] ) -> Vec < InputMessage > {
messages
. iter ( )
. filter_map ( | message | {
let role = match message . role {
MessageRole ::System | MessageRole ::User | MessageRole ::Tool = > " user " ,
MessageRole ::Assistant = > " assistant " ,
} ;
let content = message
. blocks
. iter ( )
. map ( | block | match block {
ContentBlock ::Text { text } = > InputContentBlock ::Text { text : text . clone ( ) } ,
ContentBlock ::ToolUse { id , name , input } = > InputContentBlock ::ToolUse {
id : id . clone ( ) ,
name : name . clone ( ) ,
input : serde_json ::from_str ( input )
. unwrap_or_else ( | _ | serde_json ::json! ( { " raw " : input } ) ) ,
} ,
ContentBlock ::ToolResult {
tool_use_id ,
output ,
is_error ,
..
} = > InputContentBlock ::ToolResult {
tool_use_id : tool_use_id . clone ( ) ,
content : vec ! [ ToolResultContentBlock ::Text {
text : output . clone ( ) ,
} ] ,
is_error : * is_error ,
} ,
} )
. collect ::< Vec < _ > > ( ) ;
( ! content . is_empty ( ) ) . then ( | | InputMessage {
role : role . to_string ( ) ,
content ,
} )
} )
. collect ( )
}
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
fn print_help ( ) {
2026-03-31 22:49:50 +00:00
println! ( " rusty-claude-cli v {VERSION} " ) ;
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
println! ( ) ;
2026-03-31 18:39:39 +00:00
println! ( " Usage: " ) ;
2026-03-31 23:38:53 +00:00
println! ( " rusty-claude-cli [--model MODEL] [--allowedTools TOOL[,TOOL...]] " ) ;
2026-03-31 22:49:50 +00:00
println! ( " Start the interactive REPL " ) ;
println! ( " rusty-claude-cli [--model MODEL] [--output-format text|json] prompt TEXT " ) ;
println! ( " Send one prompt and exit " ) ;
println! ( " rusty-claude-cli [--model MODEL] [--output-format text|json] TEXT " ) ;
println! ( " Shorthand non-interactive prompt mode " ) ;
2026-03-31 20:01:48 +00:00
println! ( " rusty-claude-cli --resume SESSION.json [/status] [/compact] [...] " ) ;
println! ( " Inspect or maintain a saved session without entering the REPL " ) ;
2026-03-31 18:39:39 +00:00
println! ( " rusty-claude-cli dump-manifests " ) ;
println! ( " rusty-claude-cli bootstrap-plan " ) ;
println! ( " rusty-claude-cli system-prompt [--cwd PATH] [--date YYYY-MM-DD] " ) ;
2026-03-31 23:38:05 +00:00
println! ( " rusty-claude-cli login " ) ;
println! ( " rusty-claude-cli logout " ) ;
2026-03-31 20:01:48 +00:00
println! ( ) ;
2026-03-31 22:49:50 +00:00
println! ( " Flags: " ) ;
println! ( " --model MODEL Override the active model " ) ;
println! ( " --output-format FORMAT Non-interactive output format: text or json " ) ;
2026-03-31 23:38:53 +00:00
println! ( " --allowedTools TOOLS Restrict enabled tools (repeatable; comma-separated aliases supported) " ) ;
println! ( " --version, -V Print version and build information locally " ) ;
2026-03-31 22:49:50 +00:00
println! ( ) ;
2026-03-31 20:01:48 +00:00
println! ( " Interactive slash commands: " ) ;
println! ( " {} " , render_slash_command_help ( ) ) ;
println! ( ) ;
let resume_commands = resume_supported_slash_commands ( )
. into_iter ( )
. map ( | spec | match spec . argument_hint {
Some ( argument_hint ) = > format! ( " / {} {} " , spec . name , argument_hint ) ,
None = > format! ( " / {} " , spec . name ) ,
} )
. collect ::< Vec < _ > > ( )
. join ( " , " ) ;
println! ( " Resume-safe commands: {resume_commands} " ) ;
println! ( " Examples: " ) ;
2026-03-31 22:49:50 +00:00
println! ( " rusty-claude-cli --model claude-opus \" summarize this repo \" " ) ;
println! ( " rusty-claude-cli --output-format json prompt \" explain src/main.rs \" " ) ;
2026-03-31 23:38:53 +00:00
println! ( " rusty-claude-cli --allowedTools read,glob \" summarize Cargo.toml \" " ) ;
2026-03-31 22:49:50 +00:00
println! ( " rusty-claude-cli --resume session.json /status /diff /export notes.txt " ) ;
2026-03-31 23:38:05 +00:00
println! ( " rusty-claude-cli login " ) ;
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
}
#[ cfg(test) ]
mod tests {
2026-03-31 19:27:31 +00:00
use super ::{
2026-03-31 23:38:53 +00:00
filter_tool_specs , format_compact_report , format_cost_report , format_init_report ,
format_model_report , format_model_switch_report , format_permissions_report ,
format_permissions_switch_report , format_resume_report , format_status_report ,
normalize_permission_mode , parse_args , parse_git_status_metadata , render_config_report ,
render_init_claude_md , render_memory_report , render_repl_help ,
resume_supported_slash_commands , status_context , CliAction , CliOutputFormat , SlashCommand ,
StatusUsage , DEFAULT_MODEL ,
2026-03-31 19:27:31 +00:00
} ;
2026-03-31 18:39:39 +00:00
use runtime ::{ ContentBlock , ConversationMessage , MessageRole } ;
2026-03-31 19:57:38 +00:00
use std ::path ::{ Path , PathBuf } ;
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
2026-03-31 18:39:39 +00:00
#[ test ]
fn defaults_to_repl_when_no_args ( ) {
assert_eq! (
parse_args ( & [ ] ) . expect ( " args should parse " ) ,
CliAction ::Repl {
model : DEFAULT_MODEL . to_string ( ) ,
2026-03-31 23:38:53 +00:00
allowed_tools : None ,
2026-03-31 18:39:39 +00:00
}
) ;
}
#[ test ]
fn parses_prompt_subcommand ( ) {
let args = vec! [
" prompt " . to_string ( ) ,
" hello " . to_string ( ) ,
" world " . to_string ( ) ,
] ;
assert_eq! (
parse_args ( & args ) . expect ( " args should parse " ) ,
CliAction ::Prompt {
prompt : " hello world " . to_string ( ) ,
model : DEFAULT_MODEL . to_string ( ) ,
2026-03-31 22:49:50 +00:00
output_format : CliOutputFormat ::Text ,
2026-03-31 23:38:53 +00:00
allowed_tools : None ,
2026-03-31 22:49:50 +00:00
}
) ;
}
#[ test ]
fn parses_bare_prompt_and_json_output_flag ( ) {
let args = vec! [
" --output-format=json " . to_string ( ) ,
" --model " . to_string ( ) ,
" claude-opus " . to_string ( ) ,
" explain " . to_string ( ) ,
" this " . to_string ( ) ,
] ;
assert_eq! (
parse_args ( & args ) . expect ( " args should parse " ) ,
CliAction ::Prompt {
prompt : " explain this " . to_string ( ) ,
model : " claude-opus " . to_string ( ) ,
output_format : CliOutputFormat ::Json ,
2026-03-31 23:38:53 +00:00
allowed_tools : None ,
2026-03-31 18:39:39 +00:00
}
) ;
}
2026-03-31 23:38:53 +00:00
#[ test ]
fn parses_version_flags_without_initializing_prompt_mode ( ) {
assert_eq! (
parse_args ( & [ " --version " . to_string ( ) ] ) . expect ( " args should parse " ) ,
CliAction ::Version
) ;
assert_eq! (
parse_args ( & [ " -V " . to_string ( ) ] ) . expect ( " args should parse " ) ,
CliAction ::Version
) ;
}
#[ test ]
fn parses_allowed_tools_flags_with_aliases_and_lists ( ) {
let args = vec! [
" --allowedTools " . to_string ( ) ,
" read,glob " . to_string ( ) ,
" --allowed-tools=write_file " . to_string ( ) ,
] ;
assert_eq! (
parse_args ( & args ) . expect ( " args should parse " ) ,
CliAction ::Repl {
model : DEFAULT_MODEL . to_string ( ) ,
allowed_tools : Some (
[ " glob_search " , " read_file " , " write_file " ]
. into_iter ( )
. map ( str ::to_string )
. collect ( )
) ,
}
) ;
}
#[ test ]
fn rejects_unknown_allowed_tools ( ) {
let error = parse_args ( & [ " --allowedTools " . to_string ( ) , " teleport " . to_string ( ) ] )
. expect_err ( " tool should be rejected " ) ;
assert! ( error . contains ( " unsupported tool in --allowedTools: teleport " ) ) ;
}
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
#[ test ]
fn parses_system_prompt_options ( ) {
let args = vec! [
" system-prompt " . to_string ( ) ,
" --cwd " . to_string ( ) ,
" /tmp/project " . to_string ( ) ,
" --date " . to_string ( ) ,
" 2026-04-01 " . to_string ( ) ,
] ;
assert_eq! (
parse_args ( & args ) . expect ( " args should parse " ) ,
CliAction ::PrintSystemPrompt {
cwd : PathBuf ::from ( " /tmp/project " ) ,
date : " 2026-04-01 " . to_string ( ) ,
}
) ;
}
2026-03-31 23:38:05 +00:00
#[ test ]
fn parses_login_and_logout_subcommands ( ) {
assert_eq! (
parse_args ( & [ " login " . to_string ( ) ] ) . expect ( " login should parse " ) ,
CliAction ::Login
) ;
assert_eq! (
parse_args ( & [ " logout " . to_string ( ) ] ) . expect ( " logout should parse " ) ,
CliAction ::Logout
) ;
}
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
#[ test ]
fn parses_resume_flag_with_slash_command ( ) {
let args = vec! [
" --resume " . to_string ( ) ,
" session.json " . to_string ( ) ,
" /compact " . to_string ( ) ,
] ;
assert_eq! (
parse_args ( & args ) . expect ( " args should parse " ) ,
CliAction ::ResumeSession {
session_path : PathBuf ::from ( " session.json " ) ,
2026-03-31 20:00:13 +00:00
commands : vec ! [ " /compact " . to_string ( ) ] ,
}
) ;
}
#[ test ]
fn parses_resume_flag_with_multiple_slash_commands ( ) {
let args = vec! [
" --resume " . to_string ( ) ,
" session.json " . to_string ( ) ,
" /status " . to_string ( ) ,
" /compact " . to_string ( ) ,
" /cost " . to_string ( ) ,
] ;
assert_eq! (
parse_args ( & args ) . expect ( " args should parse " ) ,
CliAction ::ResumeSession {
session_path : PathBuf ::from ( " session.json " ) ,
commands : vec ! [
" /status " . to_string ( ) ,
" /compact " . to_string ( ) ,
" /cost " . to_string ( ) ,
] ,
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
}
) ;
}
2026-03-31 18:39:39 +00:00
2026-03-31 23:38:53 +00:00
#[ test ]
fn filtered_tool_specs_respect_allowlist ( ) {
let allowed = [ " read_file " , " grep_search " ]
. into_iter ( )
. map ( str ::to_string )
. collect ( ) ;
let filtered = filter_tool_specs ( Some ( & allowed ) ) ;
let names = filtered
. into_iter ( )
. map ( | spec | spec . name )
. collect ::< Vec < _ > > ( ) ;
assert_eq! ( names , vec! [ " read_file " , " grep_search " ] ) ;
}
2026-03-31 21:03:49 +00:00
#[ test ]
fn shared_help_uses_resume_annotation_copy ( ) {
let help = commands ::render_slash_command_help ( ) ;
assert! ( help . contains ( " Slash commands " ) ) ;
assert! ( help . contains ( " works with --resume SESSION.json " ) ) ;
}
2026-03-31 19:23:05 +00:00
#[ test ]
fn repl_help_includes_shared_commands_and_exit ( ) {
let help = render_repl_help ( ) ;
2026-03-31 21:03:49 +00:00
assert! ( help . contains ( " REPL " ) ) ;
2026-03-31 19:23:05 +00:00
assert! ( help . contains ( " /help " ) ) ;
assert! ( help . contains ( " /status " ) ) ;
assert! ( help . contains ( " /model [model] " ) ) ;
2026-03-31 19:27:31 +00:00
assert! ( help . contains ( " /permissions [read-only|workspace-write|danger-full-access] " ) ) ;
2026-03-31 20:42:50 +00:00
assert! ( help . contains ( " /clear [--confirm] " ) ) ;
2026-03-31 19:27:31 +00:00
assert! ( help . contains ( " /cost " ) ) ;
2026-03-31 19:45:25 +00:00
assert! ( help . contains ( " /resume <session-path> " ) ) ;
2026-03-31 21:11:57 +00:00
assert! ( help . contains ( " /config [env|hooks|model] " ) ) ;
2026-03-31 19:54:09 +00:00
assert! ( help . contains ( " /memory " ) ) ;
2026-03-31 19:57:38 +00:00
assert! ( help . contains ( " /init " ) ) ;
2026-03-31 22:49:50 +00:00
assert! ( help . contains ( " /diff " ) ) ;
assert! ( help . contains ( " /version " ) ) ;
assert! ( help . contains ( " /export [file] " ) ) ;
assert! ( help . contains ( " /session [list|switch <session-id>] " ) ) ;
2026-03-31 19:23:05 +00:00
assert! ( help . contains ( " /exit " ) ) ;
}
2026-03-31 20:01:48 +00:00
#[ test ]
fn resume_supported_command_list_matches_expected_surface ( ) {
let names = resume_supported_slash_commands ( )
. into_iter ( )
. map ( | spec | spec . name )
. collect ::< Vec < _ > > ( ) ;
assert_eq! (
names ,
2026-03-31 22:49:50 +00:00
vec! [
" help " , " status " , " compact " , " clear " , " cost " , " config " , " memory " , " init " , " diff " ,
" version " , " export " ,
]
2026-03-31 20:01:48 +00:00
) ;
}
2026-03-31 21:04:42 +00:00
#[ test ]
fn resume_report_uses_sectioned_layout ( ) {
let report = format_resume_report ( " session.json " , 14 , 6 ) ;
assert! ( report . contains ( " Session resumed " ) ) ;
assert! ( report . contains ( " Session file session.json " ) ) ;
assert! ( report . contains ( " Messages 14 " ) ) ;
assert! ( report . contains ( " Turns 6 " ) ) ;
}
2026-03-31 21:15:37 +00:00
#[ test ]
fn compact_report_uses_structured_output ( ) {
let compacted = format_compact_report ( 8 , 5 , false ) ;
assert! ( compacted . contains ( " Compact " ) ) ;
assert! ( compacted . contains ( " Result compacted " ) ) ;
assert! ( compacted . contains ( " Messages removed 8 " ) ) ;
let skipped = format_compact_report ( 0 , 3 , true ) ;
assert! ( skipped . contains ( " Result skipped " ) ) ;
}
2026-03-31 21:02:24 +00:00
#[ test ]
fn cost_report_uses_sectioned_layout ( ) {
let report = format_cost_report ( runtime ::TokenUsage {
input_tokens : 20 ,
output_tokens : 8 ,
cache_creation_input_tokens : 3 ,
cache_read_input_tokens : 1 ,
} ) ;
assert! ( report . contains ( " Cost " ) ) ;
assert! ( report . contains ( " Input tokens 20 " ) ) ;
assert! ( report . contains ( " Output tokens 8 " ) ) ;
assert! ( report . contains ( " Cache create 3 " ) ) ;
assert! ( report . contains ( " Cache read 1 " ) ) ;
assert! ( report . contains ( " Total tokens 32 " ) ) ;
}
2026-03-31 21:01:21 +00:00
#[ test ]
fn permissions_report_uses_sectioned_layout ( ) {
let report = format_permissions_report ( " workspace-write " ) ;
assert! ( report . contains ( " Permissions " ) ) ;
2026-03-31 22:19:58 +00:00
assert! ( report . contains ( " Active mode workspace-write " ) ) ;
assert! ( report . contains ( " Modes " ) ) ;
assert! ( report . contains ( " read-only ○ available Read/search tools only " ) ) ;
assert! ( report . contains ( " workspace-write ● current Edit files inside the workspace " ) ) ;
assert! ( report . contains ( " danger-full-access ○ available Unrestricted tool access " ) ) ;
2026-03-31 21:01:21 +00:00
}
#[ test ]
fn permissions_switch_report_is_structured ( ) {
let report = format_permissions_switch_report ( " read-only " , " workspace-write " ) ;
assert! ( report . contains ( " Permissions updated " ) ) ;
2026-03-31 22:19:58 +00:00
assert! ( report . contains ( " Result mode switched " ) ) ;
assert! ( report . contains ( " Previous mode read-only " ) ) ;
assert! ( report . contains ( " Active mode workspace-write " ) ) ;
assert! ( report . contains ( " Applies to subsequent tool calls " ) ) ;
2026-03-31 21:01:21 +00:00
}
2026-03-31 21:13:27 +00:00
#[ test ]
fn init_report_uses_structured_output ( ) {
let created = format_init_report ( Path ::new ( " /tmp/CLAUDE.md " ) , true ) ;
assert! ( created . contains ( " Init " ) ) ;
assert! ( created . contains ( " Result created " ) ) ;
let skipped = format_init_report ( Path ::new ( " /tmp/CLAUDE.md " ) , false ) ;
assert! ( skipped . contains ( " skipped (already exists) " ) ) ;
}
2026-03-31 21:01:21 +00:00
2026-03-31 20:43:56 +00:00
#[ test ]
fn model_report_uses_sectioned_layout ( ) {
let report = format_model_report ( " claude-sonnet " , 12 , 4 ) ;
assert! ( report . contains ( " Model " ) ) ;
assert! ( report . contains ( " Current model claude-sonnet " ) ) ;
assert! ( report . contains ( " Session messages 12 " ) ) ;
assert! ( report . contains ( " Switch models with /model <name> " ) ) ;
}
#[ test ]
fn model_switch_report_preserves_context_summary ( ) {
let report = format_model_switch_report ( " claude-sonnet " , " claude-opus " , 9 ) ;
assert! ( report . contains ( " Model updated " ) ) ;
assert! ( report . contains ( " Previous claude-sonnet " ) ) ;
assert! ( report . contains ( " Current claude-opus " ) ) ;
assert! ( report . contains ( " Preserved msgs 9 " ) ) ;
}
2026-03-31 19:23:05 +00:00
#[ test ]
fn status_line_reports_model_and_token_totals ( ) {
2026-03-31 20:22:59 +00:00
let status = format_status_report (
2026-03-31 19:23:05 +00:00
" claude-sonnet " ,
2026-03-31 20:22:59 +00:00
StatusUsage {
message_count : 7 ,
turns : 3 ,
latest : runtime ::TokenUsage {
input_tokens : 5 ,
output_tokens : 4 ,
cache_creation_input_tokens : 1 ,
cache_read_input_tokens : 0 ,
} ,
cumulative : runtime ::TokenUsage {
input_tokens : 20 ,
output_tokens : 8 ,
cache_creation_input_tokens : 2 ,
cache_read_input_tokens : 1 ,
} ,
estimated_tokens : 128 ,
2026-03-31 19:23:05 +00:00
} ,
" workspace-write " ,
2026-03-31 20:22:59 +00:00
& super ::StatusContext {
cwd : PathBuf ::from ( " /tmp/project " ) ,
session_path : Some ( PathBuf ::from ( " session.json " ) ) ,
loaded_config_files : 2 ,
discovered_config_files : 3 ,
memory_file_count : 4 ,
2026-03-31 21:06:51 +00:00
project_root : Some ( PathBuf ::from ( " /tmp " ) ) ,
git_branch : Some ( " main " . to_string ( ) ) ,
2026-03-31 20:22:59 +00:00
} ,
2026-03-31 19:23:05 +00:00
) ;
2026-03-31 20:41:39 +00:00
assert! ( status . contains ( " Status " ) ) ;
assert! ( status . contains ( " Model claude-sonnet " ) ) ;
assert! ( status . contains ( " Permission mode workspace-write " ) ) ;
assert! ( status . contains ( " Messages 7 " ) ) ;
assert! ( status . contains ( " Latest total 10 " ) ) ;
assert! ( status . contains ( " Cumulative total 31 " ) ) ;
assert! ( status . contains ( " Cwd /tmp/project " ) ) ;
2026-03-31 21:06:51 +00:00
assert! ( status . contains ( " Project root /tmp " ) ) ;
assert! ( status . contains ( " Git branch main " ) ) ;
2026-03-31 20:41:39 +00:00
assert! ( status . contains ( " Session session.json " ) ) ;
assert! ( status . contains ( " Config files loaded 2/3 " ) ) ;
assert! ( status . contains ( " Memory files 4 " ) ) ;
}
2026-03-31 21:11:57 +00:00
#[ test ]
fn config_report_supports_section_views ( ) {
let report = render_config_report ( Some ( " env " ) ) . expect ( " config report should render " ) ;
assert! ( report . contains ( " Merged section: env " ) ) ;
}
2026-03-31 21:08:19 +00:00
#[ test ]
fn memory_report_uses_sectioned_layout ( ) {
let report = render_memory_report ( ) . expect ( " memory report should render " ) ;
assert! ( report . contains ( " Memory " ) ) ;
assert! ( report . contains ( " Working directory " ) ) ;
assert! ( report . contains ( " Instruction files " ) ) ;
assert! ( report . contains ( " Discovered files " ) ) ;
}
2026-03-31 20:41:39 +00:00
#[ test ]
fn config_report_uses_sectioned_layout ( ) {
2026-03-31 21:11:57 +00:00
let report = render_config_report ( None ) . expect ( " config report should render " ) ;
2026-03-31 20:41:39 +00:00
assert! ( report . contains ( " Config " ) ) ;
assert! ( report . contains ( " Discovered files " ) ) ;
assert! ( report . contains ( " Merged JSON " ) ) ;
2026-03-31 20:22:59 +00:00
}
#[ test ]
2026-03-31 21:06:51 +00:00
fn parses_git_status_metadata ( ) {
let ( root , branch ) = parse_git_status_metadata ( Some (
" ## rcc/cli...origin/rcc/cli
M src / main . rs " ,
) ) ;
assert_eq! ( branch . as_deref ( ) , Some ( " rcc/cli " ) ) ;
let _ = root ;
}
#[ test ]
2026-03-31 20:22:59 +00:00
fn status_context_reads_real_workspace_metadata ( ) {
let context = status_context ( None ) . expect ( " status context should load " ) ;
assert! ( context . cwd . is_absolute ( ) ) ;
assert_eq! ( context . discovered_config_files , 3 ) ;
assert! ( context . loaded_config_files < = context . discovered_config_files ) ;
2026-03-31 19:23:05 +00:00
}
2026-03-31 19:27:31 +00:00
#[ test ]
fn normalizes_supported_permission_modes ( ) {
assert_eq! ( normalize_permission_mode ( " read-only " ) , Some ( " read-only " ) ) ;
assert_eq! (
normalize_permission_mode ( " workspace-write " ) ,
Some ( " workspace-write " )
) ;
assert_eq! (
normalize_permission_mode ( " danger-full-access " ) ,
Some ( " danger-full-access " )
) ;
assert_eq! ( normalize_permission_mode ( " unknown " ) , None ) ;
}
2026-03-31 20:42:50 +00:00
#[ test ]
fn clear_command_requires_explicit_confirmation_flag ( ) {
assert_eq! (
SlashCommand ::parse ( " /clear " ) ,
Some ( SlashCommand ::Clear { confirm : false } )
) ;
assert_eq! (
SlashCommand ::parse ( " /clear --confirm " ) ,
Some ( SlashCommand ::Clear { confirm : true } )
) ;
}
2026-03-31 19:45:25 +00:00
#[ test ]
fn parses_resume_and_config_slash_commands ( ) {
assert_eq! (
SlashCommand ::parse ( " /resume saved-session.json " ) ,
Some ( SlashCommand ::Resume {
session_path : Some ( " saved-session.json " . to_string ( ) )
} )
) ;
2026-03-31 20:42:50 +00:00
assert_eq! (
SlashCommand ::parse ( " /clear --confirm " ) ,
Some ( SlashCommand ::Clear { confirm : true } )
) ;
2026-03-31 21:11:57 +00:00
assert_eq! (
SlashCommand ::parse ( " /config " ) ,
Some ( SlashCommand ::Config { section : None } )
) ;
assert_eq! (
SlashCommand ::parse ( " /config env " ) ,
Some ( SlashCommand ::Config {
section : Some ( " env " . to_string ( ) )
} )
) ;
2026-03-31 19:54:09 +00:00
assert_eq! ( SlashCommand ::parse ( " /memory " ) , Some ( SlashCommand ::Memory ) ) ;
2026-03-31 19:57:38 +00:00
assert_eq! ( SlashCommand ::parse ( " /init " ) , Some ( SlashCommand ::Init ) ) ;
}
#[ test ]
fn init_template_mentions_detected_rust_workspace ( ) {
let rendered = render_init_claude_md ( Path ::new ( " . " ) ) ;
assert! ( rendered . contains ( " # CLAUDE.md " ) ) ;
assert! ( rendered . contains ( " cargo clippy --workspace --all-targets -- -D warnings " ) ) ;
2026-03-31 19:45:25 +00:00
}
2026-03-31 18:39:39 +00:00
#[ test ]
fn converts_tool_roundtrip_messages ( ) {
let messages = vec! [
ConversationMessage ::user_text ( " hello " ) ,
ConversationMessage ::assistant ( vec! [ ContentBlock ::ToolUse {
id : " tool-1 " . to_string ( ) ,
name : " bash " . to_string ( ) ,
input : " { \" command \" : \" pwd \" } " . to_string ( ) ,
} ] ) ,
ConversationMessage {
role : MessageRole ::Tool ,
blocks : vec ! [ ContentBlock ::ToolResult {
tool_use_id : " tool-1 " . to_string ( ) ,
tool_name : " bash " . to_string ( ) ,
output : " ok " . to_string ( ) ,
is_error : false ,
} ] ,
usage : None ,
} ,
] ;
let converted = super ::convert_messages ( & messages ) ;
assert_eq! ( converted . len ( ) , 3 ) ;
assert_eq! ( converted [ 1 ] . role , " assistant " ) ;
assert_eq! ( converted [ 2 ] . role , " user " ) ;
}
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
}