repositories / smith
smith
There are many coding harnesses - but this one is fast
owned by admin
smith-cli/src/main.rs
Raw#![forbid(unsafe_code)]
#![warn(missing_docs)]
//! Smith command-line entry point.
//!
//! Two modes today: `eval` runs one prompt through the agent and prints the
//! result; `rpc` speaks LF-delimited JSON-RPC on standard input and output.
//! Both use the same runtime assembly from `smith-harness`.
use clap::{CommandFactory, FromArgMatches, Parser, ValueEnum};
use smith::error::Result;
use smith_harness::runtime::{RuntimeConfig, RuntimeProvider, build_agent};
use smith_harness::{AuthStore, ProviderKind, credential_env_var};
use smith_rpc::{RpcState, handle_line};
use std::io::{BufRead, Write};
/// Every allocation is charged to the current lifetime scope (`SMH-SPEC-SPEC0001`, memory accounting).
#[global_allocator]
static ALLOC: smith_alloc::Counting = smith_alloc::Counting;
/// Smith, a fast coding-agent runtime.
#[derive(Debug, Parser)]
#[command(name = "smith", version, about)]
struct Cli {
#[command(subcommand)]
command: Command,
}
/// Interface mode.
#[derive(Debug, Parser)]
enum Command {
/// Manage stored provider credentials.
Auth {
#[command(subcommand)]
action: AuthAction,
},
/// Run one prompt without the TUI.
Eval {
/// The prompt to run.
prompt: String,
/// Print the structured outcome as JSON.
#[arg(long)]
json: bool,
/// Use the scripted mock provider instead of a real endpoint.
#[arg(long)]
r#mock: bool,
/// Provider kind; required for real endpoints.
#[arg(long, env = "SMITH_PROVIDER")]
provider: Option<ProviderArg>,
/// Model id; required for real endpoints.
#[arg(long, env = "SMITH_MODEL")]
model: Option<String>,
/// Endpoint base URL override.
#[arg(long, env = "SMITH_BASE_URL")]
base_url: Option<String>,
},
/// Serve LF-delimited JSON-RPC on standard input and output.
Rpc {
/// Provider kind; the mock provider serves when unset.
#[arg(long, env = "SMITH_PROVIDER")]
provider: Option<ProviderArg>,
/// Model id; required for real endpoints.
#[arg(long, env = "SMITH_MODEL")]
model: Option<String>,
/// Endpoint base URL override.
#[arg(long, env = "SMITH_BASE_URL")]
base_url: Option<String>,
},
}
/// Credential operations.
#[derive(Debug, Parser)]
enum AuthAction {
/// Store a credential read from standard input.
Add {
/// Provider the credential belongs to.
#[arg(long)]
provider: ProviderArg,
},
/// List providers with stored credentials.
List,
/// Delete one stored credential.
Remove {
/// Provider whose credential is deleted.
#[arg(long)]
provider: ProviderArg,
},
/// Resolve one provider's credential without any network access.
Check {
/// Provider whose credential is checked.
#[arg(long)]
provider: ProviderArg,
},
}
/// Provider names accepted on the command line.
#[derive(Copy, Clone, Debug, ValueEnum)]
enum ProviderArg {
/// OpenAI-compatible endpoints.
Openai,
/// Anthropic Messages API.
Anthropic,
/// Google Gemini API.
Google,
}
impl ProviderArg {
const fn kind(self) -> ProviderKind {
match self {
Self::Openai => ProviderKind::OpenAiCompatible,
Self::Anthropic => ProviderKind::Anthropic,
Self::Google => ProviderKind::Google,
}
}
}
#[expect(clippy::print_stderr, reason = "the CLI reports failures on stderr")]
fn main() -> std::process::ExitCode {
let matches = Cli::command().get_matches();
let cli = match Cli::from_arg_matches(&matches) {
Ok(cli) => cli,
Err(error) => {
error.exit();
}
};
let result = match cli.command {
Command::Auth { action } => auth(&action),
Command::Eval {
prompt,
json,
r#mock,
provider,
model,
base_url,
} => eval(
&prompt,
json,
r#mock,
provider,
model.as_deref(),
base_url,
&subcommand_sources(&matches, "eval"),
),
Command::Rpc {
provider,
model,
base_url,
} => rpc(provider, model, base_url),
};
match result {
Ok(()) => std::process::ExitCode::SUCCESS,
Err(err) => {
eprintln!("smith: {err}");
std::process::ExitCode::FAILURE
}
}
}
#[expect(clippy::print_stdout, reason = "auth command output is the product")]
fn auth(action: &AuthAction) -> Result<()> {
let store = AuthStore::at_default();
match action {
AuthAction::Add { provider } => {
let mut secret = String::new();
std::io::stdin()
.lock()
.read_line(&mut secret)
.map_err(|e| smith::error::SmithError::Session {
code: "AUTH_READ".to_string(),
message: e.to_string(),
})?;
let secret = secret.trim_end_matches(['\r', '\n']);
if secret.is_empty() {
return Err(smith::error::SmithError::Provider {
fault: smith::error::ProviderFault::Authentication {
message: "refusing to store an empty credential".to_string(),
},
});
}
store.save(provider.kind(), secret)?;
println!("{}: stored", provider.kind().slug());
}
AuthAction::List => {
for kind in store.list()? {
println!("{}: store", kind.slug());
}
}
AuthAction::Remove { provider } => {
if store.remove(provider.kind())? {
println!("{}: removed", provider.kind().slug());
} else {
return Err(smith::error::SmithError::Provider {
fault: smith::error::ProviderFault::Authentication {
message: format!("no stored credential for {}", provider.kind().slug()),
},
});
}
}
AuthAction::Check { provider } => {
let kind = provider.kind();
let var = credential_env_var(kind);
if store.load(kind)?.is_some() {
println!("{}: store", kind.slug());
} else if std::env::var(var).is_ok_and(|value| !value.is_empty()) {
println!("{}: env:{var}", kind.slug());
} else {
return Err(smith::error::SmithError::Provider {
fault: smith::error::ProviderFault::Authentication {
message: format!(
"no credential for {} (tried store, env {var})",
kind.slug()
),
},
});
}
}
}
Ok(())
}
/// Which surface supplied each selection option, for provenance reports.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum OptionSource {
/// An explicit command-line flag.
Cli,
/// A `SMITH_` environment variable.
Env,
/// Nothing supplied a value.
Unset,
}
fn source_of(matches: &clap::ArgMatches, field: &str) -> OptionSource {
match matches.value_source(field) {
Some(clap::parser::ValueSource::CommandLine) => OptionSource::Cli,
Some(clap::parser::ValueSource::EnvVariable) => OptionSource::Env,
_ => OptionSource::Unset,
}
}
fn subcommand_sources(matches: &clap::ArgMatches, name: &str) -> SelectionSources {
let sub = matches.subcommand_matches(name);
SelectionSources {
provider: sub.map_or(OptionSource::Unset, |m| source_of(m, "provider")),
model: sub.map_or(OptionSource::Unset, |m| source_of(m, "model")),
base_url: sub.map_or(OptionSource::Unset, |m| source_of(m, "base_url")),
}
}
/// Provenance of each selection option.
struct SelectionSources {
provider: OptionSource,
model: OptionSource,
base_url: OptionSource,
}
impl SelectionSources {
fn describe(&self, provider: ProviderArg, model: &str) -> String {
let provider = provider.slug();
format!(
"provider={provider}({}) model={model}({}) base-url({})",
self.provider.tag(),
self.model.tag(),
self.base_url.tag()
)
}
}
impl OptionSource {
const fn tag(self) -> &'static str {
match self {
Self::Cli => "cli",
Self::Env => "env",
Self::Unset => "unset",
}
}
}
impl ProviderArg {
const fn slug(self) -> &'static str {
self.kind().slug()
}
}
fn real_provider(
provider: Option<ProviderArg>,
model: Option<String>,
base_url: Option<String>,
) -> Result<RuntimeProvider> {
let Some(provider) = provider else {
return Err(smith::error::SmithError::Provider {
fault: smith::error::ProviderFault::Authentication {
message: "no provider selected (tried --provider/SMITH_PROVIDER)".to_string(),
},
});
};
let Some(model) = model else {
return Err(smith::error::SmithError::Provider {
fault: smith::error::ProviderFault::Authentication {
message: format!(
"no model selected for {} (tried --model/SMITH_MODEL)",
provider.slug()
),
},
});
};
Ok(RuntimeProvider::real(provider.kind(), base_url, model))
}
fn provider(
mock: bool,
provider: Option<ProviderArg>,
model: Option<String>,
base_url: Option<String>,
) -> Result<RuntimeProvider> {
if mock {
return Ok(RuntimeProvider::Mock(vec![smith_harness::mock_text_reply(
"mock provider: no model configured",
)]));
}
real_provider(provider, model, base_url)
}
#[expect(clippy::print_stdout, reason = "eval output is the command's product")]
#[expect(
clippy::print_stderr,
reason = "selection provenance reports on stderr"
)]
fn eval(
prompt: &str,
json: bool,
mock: bool,
selection: Option<ProviderArg>,
model: Option<&str>,
base_url: Option<String>,
sources: &SelectionSources,
) -> Result<()> {
let runtime_provider = provider(mock, selection, model.map(String::from), base_url)?;
if let Some(provider) = selection
&& let Some(model) = &model
{
eprintln!("smith: {}", sources.describe(provider, model));
}
let dir = session_dir()?;
let config = RuntimeConfig {
provider: runtime_provider,
workdir: dir.clone(),
session_path: dir.join("eval.smh"),
api_key: None,
auth_dir: smith::config::auth_dir(),
};
let mut agent = build_agent(&config)?;
let outcome = {
let _turn = smith_alloc::enter(smith_alloc::Scope::Turn);
agent.run_turn(prompt)
}?;
if json {
println!(
"{}",
serde_json::json!({
"text": outcome.text,
"reason": format!("{:?}", outcome.reason),
"cost": {
"input_tokens": outcome.cost.input_tokens,
"output_tokens": outcome.cost.output_tokens,
},
})
);
} else {
println!("{}", outcome.text);
}
Ok(())
}
fn rpc(
provider: Option<ProviderArg>,
model: Option<String>,
base_url: Option<String>,
) -> Result<()> {
let runtime_provider = match provider {
Some(provider) => real_provider(Some(provider), model, base_url)?,
None => RuntimeProvider::Mock(vec![smith_harness::mock_text_reply(
"rpc mock provider ready",
)]),
};
let dir = session_dir()?;
let config = RuntimeConfig {
provider: runtime_provider,
workdir: dir.clone(),
session_path: dir.join("rpc.smh"),
api_key: None,
auth_dir: smith::config::auth_dir(),
};
let mut state = RpcState::new(build_agent(&config)?);
let (input_tx, input_rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
for line in std::io::stdin().lock().lines() {
if input_tx.send(Some(line)).is_err() {
return;
}
}
let _ = input_tx.send(None);
});
let mut stdout = std::io::stdout();
let mut input_closed = false;
loop {
match input_rx.recv_timeout(std::time::Duration::from_millis(10)) {
Ok(Some(Ok(line))) => {
for response in handle_line(&mut state, &line) {
stdout
.write_all(response.as_bytes())
.map_err(rpc_stdout_error)?;
}
}
Ok(Some(Err(error))) => {
return Err(smith::error::SmithError::Session {
code: "RPC_STDIN".to_string(),
message: error.to_string(),
});
}
Ok(None) | Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
input_closed = true;
}
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {}
}
for response in state.drain_lines() {
stdout
.write_all(response.as_bytes())
.map_err(rpc_stdout_error)?;
}
stdout.flush().map_err(rpc_stdout_error)?;
if input_closed && state.is_settled() {
for response in state.drain_lines() {
stdout
.write_all(response.as_bytes())
.map_err(rpc_stdout_error)?;
}
return Ok(());
}
}
}
fn rpc_stdout_error(error: std::io::Error) -> smith::error::SmithError {
let message = error.to_string();
drop(error);
smith::error::SmithError::Session {
code: "RPC_STDOUT".to_string(),
message,
}
}
fn session_dir() -> Result<std::path::PathBuf> {
let config = smith::config::Config::default_valid();
let dir = std::path::PathBuf::from(config.session_dir).join("run");
std::fs::create_dir_all(&dir).map_err(|e| smith::error::SmithError::Session {
code: "RUNTIME_DIR".to_string(),
message: e.to_string(),
})?;
Ok(dir)
}