use std::io::Read;
use std::sync::Arc;
use clap::{Parser, ValueEnum};
use deepseek::agent::builtin_tools::default_tools;
use deepseek::{
ContentBlock, PermissionMode, ResultSubtype, RunOptions, SdkMessage, SystemSubtype,
};
use deepseek::{ReqwestClient};
use deepseek::types::EffortLevel;
use futures::StreamExt;
#[derive(Parser, Debug)]
#[command(name = "deepseek", about = "DeepSeek agent loop — Claude-Code shape")]
struct Args {
#[arg(trailing_var_arg = true)]
prompt: Vec<String>,
#[arg(short, long, default_value = "deepseek-v4-pro")]
model: String,
#[arg(long)]
max_turns: Option<u32>,
#[arg(long)]
max_budget_usd: Option<f64>,
#[arg(long, value_enum, default_value_t = CliEffort::High)]
effort: CliEffort,
#[arg(long, value_enum, default_value_t = CliPermissionMode::AcceptEdits)]
permission_mode: CliPermissionMode,
#[arg(long, value_delimiter = ',')]
allowed_tools: Option<Vec<String>>,
#[arg(long, value_delimiter = ',', default_value = "")]
disallowed_tools: Vec<String>,
#[arg(long)]
system_prompt: Option<String>,
#[arg(long)]
base_url: Option<String>,
#[arg(long)]
json: bool,
#[arg(long)]
api_key: Option<String>,
}
#[derive(Copy, Clone, Debug, ValueEnum)]
enum CliEffort {
Low,
Medium,
High,
Max,
}
impl From<CliEffort> for EffortLevel {
fn from(v: CliEffort) -> Self {
match v {
CliEffort::Low => EffortLevel::Low,
CliEffort::Medium => EffortLevel::Medium,
CliEffort::High => EffortLevel::High,
CliEffort::Max => EffortLevel::Max,
}
}
}
#[derive(Copy, Clone, Debug, ValueEnum)]
enum CliPermissionMode {
Default,
AcceptEdits,
Plan,
DontAsk,
BypassPermissions,
}
impl From<CliPermissionMode> for PermissionMode {
fn from(v: CliPermissionMode) -> Self {
match v {
CliPermissionMode::Default => PermissionMode::Default,
CliPermissionMode::AcceptEdits => PermissionMode::AcceptEdits,
CliPermissionMode::Plan => PermissionMode::Plan,
CliPermissionMode::DontAsk => PermissionMode::DontAsk,
CliPermissionMode::BypassPermissions => PermissionMode::BypassPermissions,
}
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let _ = dotenvy::dotenv();
let args = Args::parse();
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn,deepseek=info")),
)
.with_writer(std::io::stderr)
.init();
let api_key = args
.api_key
.clone()
.or_else(|| std::env::var("DEEPSEEK_API_KEY").ok())
.ok_or_else(|| {
anyhow::anyhow!("DEEPSEEK_API_KEY not set and --api-key not provided")
})?;
let prompt = if args.prompt.is_empty() {
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf)?;
buf.trim().to_string()
} else {
args.prompt.join(" ")
};
if prompt.is_empty() {
anyhow::bail!("empty prompt; pass it as positional args or via stdin");
}
let mut opts = RunOptions::new(&args.model)
.effort(args.effort.into())
.permission_mode(args.permission_mode.into());
if let Some(n) = args.max_turns {
opts = opts.max_turns(n);
}
if let Some(b) = args.max_budget_usd {
opts = opts.max_budget_usd(b);
}
if let Some(allowed) = args.allowed_tools {
opts = opts.allowed_tools(allowed);
}
if !args.disallowed_tools.is_empty() {
opts = opts.disallowed_tools(args.disallowed_tools);
}
if let Some(sp) = args.system_prompt {
opts = opts.system_prompt(sp);
} else {
opts = opts.system_prompt(default_system_prompt());
}
if let Some(b) = args.base_url {
opts = opts.base_url(b);
}
let http = ReqwestClient::new();
let tools = Arc::new(default_tools());
let mut stream =
Box::pin(deepseek::run(http, api_key, tools, prompt, opts));
while let Some(msg) = stream.next().await {
if args.json {
println!("{}", serde_json::to_string(&msg)?);
} else {
print_pretty(&msg);
}
if msg.is_terminal() {
break;
}
}
Ok(())
}
fn default_system_prompt() -> String {
"You are a coding agent running in a developer's terminal. Use the available tools \
(Read, Write, Edit, Glob, Grep, Bash) to inspect and modify the working directory. \
Be concise; explain only when asked."
.into()
}
fn print_pretty(msg: &SdkMessage) {
match msg {
SdkMessage::System {
subtype: SystemSubtype::Init,
session_id,
..
} => {
eprintln!("● session {session_id}");
}
SdkMessage::Assistant { content, .. } => {
for block in content {
match block {
ContentBlock::Text { text } if !text.is_empty() => {
println!("{text}");
}
ContentBlock::ToolUse { name, input, .. } => {
let preview = serde_json::to_string(input).unwrap_or_default();
let preview = if preview.len() > 120 {
format!("{}…", &preview[..120])
} else {
preview
};
eprintln!("▸ {name} {preview}");
}
_ => {}
}
}
}
SdkMessage::User { content } => {
for block in content {
if let ContentBlock::ToolResult {
tool_use_id,
content,
is_error,
} = block
{
let head = content.lines().next().unwrap_or("");
let marker = if *is_error { "✗" } else { "✓" };
eprintln!(" {marker} {tool_use_id}: {head}");
}
}
}
SdkMessage::Result {
subtype,
num_turns,
total_cost_usd,
..
} => {
let cost = total_cost_usd
.map(|c| format!("${c:.4}"))
.unwrap_or_else(|| "n/a".into());
let label = match subtype {
ResultSubtype::Success => "✓ success",
ResultSubtype::ErrorMaxTurns => "✗ max_turns",
ResultSubtype::ErrorMaxBudgetUsd => "✗ max_budget_usd",
ResultSubtype::ErrorDuringExecution => "✗ runtime_error",
};
eprintln!("{label} | turns={num_turns} | cost={cost}");
}
}
}