use std::io::{self, IsTerminal, Read, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::Duration;
use anyhow::{Context, Result, anyhow, bail};
use clap::{Args, CommandFactory, Parser, Subcommand};
use clap_complete::{Shell, generate};
use dotenvy::dotenv;
use tempfile::NamedTempFile;
use wait_timeout::ChildExt;
mod audit;
mod automation_manager;
mod client;
mod command_safety;
mod commands;
mod compaction;
mod config;
mod core;
mod cycle_manager;
mod deepseek_theme;
mod error_taxonomy;
mod eval;
mod execpolicy;
mod features;
mod hooks;
mod llm_client;
mod logging;
mod lsp;
mod mcp;
mod mcp_server;
mod models;
mod network_policy;
mod palette;
mod pricing;
mod project_context;
mod project_doc;
mod prompts;
pub mod repl;
mod responses_api_proxy;
pub mod rlm;
mod runtime_api;
mod runtime_threads;
mod sandbox;
mod seam_manager;
mod session_manager;
mod settings;
mod skills;
mod snapshot;
mod task_manager;
#[cfg(test)]
mod test_support;
mod tools;
mod tui;
mod ui;
mod utils;
mod working_set;
mod workspace_trust;
use crate::config::{Config, DEFAULT_TEXT_MODEL, MAX_SUBAGENTS};
use crate::eval::{EvalHarness, EvalHarnessConfig, ScenarioStepKind};
use crate::features::Feature;
use crate::llm_client::LlmClient;
use crate::mcp::{McpConfig, McpPool, McpServerConfig};
use crate::models::{ContentBlock, Message, MessageRequest, SystemPrompt};
use crate::session_manager::{SessionManager, create_saved_session};
use crate::tui::history::{summarize_tool_args, summarize_tool_output};
#[derive(Parser, Debug)]
#[command(
name = "deepseek",
author,
version,
about = "DeepSeek TUI/CLI for DeepSeek models",
long_about = "Terminal-native TUI and CLI for DeepSeek models.\n\nRun 'deepseek' to start.\n\nNot affiliated with DeepSeek Inc."
)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
#[command(flatten)]
feature_toggles: FeatureToggles,
#[arg(short, long)]
prompt: Option<String>,
#[arg(long)]
yolo: bool,
#[arg(long)]
max_subagents: Option<usize>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(short, long)]
verbose: bool,
#[arg(long)]
profile: Option<String>,
#[arg(short, long)]
workspace: Option<PathBuf>,
#[arg(short, long)]
resume: Option<String>,
#[arg(short = 'c', long = "continue")]
continue_session: bool,
#[arg(long = "no-alt-screen")]
no_alt_screen: bool,
#[arg(long = "mouse-capture", conflicts_with = "no_mouse_capture")]
mouse_capture: bool,
#[arg(long = "no-mouse-capture", conflicts_with = "mouse_capture")]
no_mouse_capture: bool,
#[arg(long)]
skip_onboarding: bool,
}
#[derive(Subcommand, Debug, Clone)]
#[allow(clippy::large_enum_variant)]
enum Commands {
Doctor(DoctorArgs),
Setup(SetupArgs),
Completions {
#[arg(value_enum)]
shell: Shell,
},
Sessions {
#[arg(short, long, default_value = "20")]
limit: usize,
#[arg(short, long)]
search: Option<String>,
},
Init,
Login {
#[arg(long)]
api_key: Option<String>,
},
Logout,
Models(ModelsArgs),
Exec(ExecArgs),
Review(ReviewArgs),
Apply(ApplyArgs),
Eval(EvalArgs),
Mcp {
#[command(subcommand)]
command: McpCommand,
},
Execpolicy(ExecpolicyCommand),
Features(FeaturesCli),
Sandbox(SandboxArgs),
Serve(ServeArgs),
Resume {
#[arg(value_name = "SESSION_ID")]
session_id: Option<String>,
#[arg(long = "last", default_value_t = false, conflicts_with = "session_id")]
last: bool,
},
Fork {
#[arg(value_name = "SESSION_ID")]
session_id: Option<String>,
#[arg(long = "last", default_value_t = false, conflicts_with = "session_id")]
last: bool,
},
#[command(hide = true)]
ResponsesApiProxy(responses_api_proxy::Args),
}
#[derive(Args, Debug, Clone)]
struct ExecArgs {
prompt: String,
#[arg(long)]
model: Option<String>,
#[arg(long, default_value_t = false)]
auto: bool,
#[arg(long, default_value_t = false)]
json: bool,
}
#[derive(Args, Debug, Clone, Default)]
struct SetupArgs {
#[arg(long, default_value_t = false)]
mcp: bool,
#[arg(long, default_value_t = false)]
skills: bool,
#[arg(long, default_value_t = false)]
tools: bool,
#[arg(long, default_value_t = false)]
plugins: bool,
#[arg(long, default_value_t = false)]
all: bool,
#[arg(long, default_value_t = false)]
local: bool,
#[arg(long, default_value_t = false)]
force: bool,
#[arg(long, default_value_t = false, conflicts_with_all = ["mcp", "skills", "tools", "plugins", "all", "local", "clean"])]
status: bool,
#[arg(long, default_value_t = false, conflicts_with_all = ["mcp", "skills", "tools", "plugins", "all", "local", "status"])]
clean: bool,
}
#[derive(Args, Debug, Clone, Default)]
struct DoctorArgs {
#[arg(long, default_value_t = false)]
json: bool,
}
#[derive(Args, Debug, Clone)]
struct EvalArgs {
#[arg(long, value_name = "STEP")]
fail_step: Option<String>,
#[arg(long, default_value = "printf eval-harness")]
shell_command: String,
#[arg(long, default_value = "eval-harness")]
shell_expect_token: String,
#[arg(long, default_value_t = 240)]
max_output_chars: usize,
#[arg(long, default_value_t = false)]
json: bool,
#[arg(long, value_name = "DIR")]
record: Option<PathBuf>,
}
#[derive(Args, Debug, Clone, Default)]
struct ModelsArgs {
#[arg(long, default_value_t = false)]
json: bool,
}
#[derive(Args, Debug, Default, Clone)]
struct FeatureToggles {
#[arg(long = "enable", value_name = "FEATURE", action = clap::ArgAction::Append, global = true)]
enable: Vec<String>,
#[arg(long = "disable", value_name = "FEATURE", action = clap::ArgAction::Append, global = true)]
disable: Vec<String>,
}
impl FeatureToggles {
fn apply(&self, config: &mut Config) -> Result<()> {
for feature in &self.enable {
config.set_feature(feature, true)?;
}
for feature in &self.disable {
config.set_feature(feature, false)?;
}
Ok(())
}
}
#[derive(Args, Debug, Clone)]
struct ReviewArgs {
#[arg(long, conflicts_with = "base")]
staged: bool,
#[arg(long)]
base: Option<String>,
#[arg(long)]
path: Option<PathBuf>,
#[arg(long)]
model: Option<String>,
#[arg(long, default_value_t = 200_000)]
max_chars: usize,
#[arg(long, default_value_t = false)]
json: bool,
}
#[derive(Args, Debug, Clone)]
struct ApplyArgs {
#[arg(value_name = "PATCH_FILE")]
patch_file: Option<PathBuf>,
}
#[derive(Args, Debug, Clone)]
struct ServeArgs {
#[arg(long)]
mcp: bool,
#[arg(long)]
http: bool,
#[arg(long, default_value = "127.0.0.1")]
host: String,
#[arg(long, default_value_t = 7878)]
port: u16,
#[arg(long, default_value_t = 2)]
workers: usize,
}
#[derive(Subcommand, Debug, Clone)]
enum McpCommand {
List,
Init {
#[arg(long, default_value_t = false)]
force: bool,
},
Connect {
#[arg(value_name = "SERVER")]
server: Option<String>,
},
Tools {
#[arg(value_name = "SERVER")]
server: Option<String>,
},
Add {
name: String,
#[arg(long, conflicts_with = "url")]
command: Option<String>,
#[arg(long, conflicts_with = "command")]
url: Option<String>,
#[arg(long = "arg")]
args: Vec<String>,
},
Remove {
name: String,
},
Enable {
name: String,
},
Disable {
name: String,
},
Validate,
#[command(
name = "add-self",
long_about = "Register this DeepSeek binary as a local MCP stdio server.\n\nAdds a config entry to ~/.deepseek/mcp.json that launches `deepseek serve --mcp`\nvia the stdio transport. Other DeepSeek sessions (or any MCP client) can then\ndiscover and call tools exposed by this server.\n\nUse `deepseek serve --http` instead if you need the HTTP/SSE runtime API."
)]
AddSelf {
#[arg(long, default_value = "deepseek")]
name: String,
#[arg(long)]
workspace: Option<String>,
},
}
#[derive(Args, Debug, Clone)]
struct ExecpolicyCommand {
#[command(subcommand)]
command: ExecpolicySubcommand,
}
#[derive(Subcommand, Debug, Clone)]
enum ExecpolicySubcommand {
Check(execpolicy::ExecPolicyCheckCommand),
}
#[derive(Args, Debug, Clone)]
struct FeaturesCli {
#[command(subcommand)]
command: FeaturesSubcommand,
}
#[derive(Subcommand, Debug, Clone)]
enum FeaturesSubcommand {
List,
}
#[derive(Args, Debug, Clone)]
struct SandboxArgs {
#[command(subcommand)]
command: SandboxCommand,
}
#[derive(Subcommand, Debug, Clone)]
enum SandboxCommand {
Run {
#[arg(long, default_value = "workspace-write")]
policy: String,
#[arg(long)]
network: bool,
#[arg(long, value_name = "PATH")]
writable_root: Vec<PathBuf>,
#[arg(long)]
exclude_tmpdir: bool,
#[arg(long)]
exclude_slash_tmp: bool,
#[arg(long)]
cwd: Option<PathBuf>,
#[arg(long, default_value_t = 60_000)]
timeout_ms: u64,
#[arg(required = true, trailing_var_arg = true)]
command: Vec<String>,
},
}
#[tokio::main]
async fn main() -> Result<()> {
dotenv().ok();
let cli = Cli::parse();
logging::set_verbose(cli.verbose || logging::env_requests_verbose_logging());
if let Some(command) = cli.command.clone() {
return match command {
Commands::Doctor(args) => {
let config = load_config_from_cli(&cli)?;
let workspace = resolve_workspace(&cli);
if args.json {
run_doctor_json(&config, &workspace, cli.config.as_deref())
} else {
run_doctor(&config, &workspace, cli.config.as_deref()).await;
Ok(())
}
}
Commands::Setup(args) => {
let config = load_config_from_cli(&cli)?;
let workspace = resolve_workspace(&cli);
run_setup(&config, &workspace, args)
}
Commands::Completions { shell } => {
generate_completions(shell);
Ok(())
}
Commands::Sessions { limit, search } => list_sessions(limit, search),
Commands::Init => init_project(),
Commands::Login { api_key } => run_login(api_key),
Commands::Logout => run_logout(),
Commands::Models(args) => {
let config = load_config_from_cli(&cli)?;
run_models(&config, args).await
}
Commands::Exec(args) => {
let config = load_config_from_cli(&cli)?;
let model = args
.model
.or_else(|| config.default_text_model.clone())
.unwrap_or_else(|| config.default_model());
if args.auto || cli.yolo {
let workspace = cli.workspace.clone().unwrap_or_else(|| {
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
});
let max_subagents = cli.max_subagents.map_or_else(
|| config.max_subagents(),
|value| value.clamp(1, MAX_SUBAGENTS),
);
let auto_mode = args.auto || cli.yolo;
run_exec_agent(
&config,
&model,
&args.prompt,
workspace,
max_subagents,
true,
auto_mode,
args.json,
)
.await
} else if args.json {
run_one_shot_json(&config, &model, &args.prompt).await
} else {
run_one_shot(&config, &model, &args.prompt).await
}
}
Commands::Review(args) => {
let config = load_config_from_cli(&cli)?;
run_review(&config, args).await
}
Commands::Apply(args) => run_apply(args),
Commands::Eval(args) => run_eval(args),
Commands::Mcp { command } => {
let config = load_config_from_cli(&cli)?;
run_mcp_command(&config, command).await
}
Commands::Execpolicy(command) => {
let config = load_config_from_cli(&cli)?;
if !config.features().enabled(Feature::ExecPolicy) {
bail!(
"The `exec_policy` feature is disabled. Enable it in [features] or via profile."
);
}
run_execpolicy_command(command)
}
Commands::Features(command) => {
let config = load_config_from_cli(&cli)?;
run_features_command(&config, command)
}
Commands::Sandbox(args) => run_sandbox_command(args),
Commands::Serve(args) => {
let workspace = cli.workspace.clone().unwrap_or_else(|| {
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
});
if args.mcp && args.http {
bail!("Choose exactly one server mode: --mcp or --http");
}
if args.mcp {
mcp_server::run_mcp_server(workspace)
} else if args.http {
let config = load_config_from_cli(&cli)?;
runtime_api::run_http_server(
config,
workspace,
runtime_api::RuntimeApiOptions {
host: args.host,
port: args.port,
workers: args.workers.clamp(1, 8),
},
)
.await
} else {
bail!("No server mode specified. Use --mcp or --http.")
}
}
Commands::Resume { session_id, last } => {
let config = load_config_from_cli(&cli)?;
let resume_id = resolve_session_id(session_id, last)?;
run_interactive(&cli, &config, Some(resume_id)).await
}
Commands::Fork { session_id, last } => {
let config = load_config_from_cli(&cli)?;
let new_session_id = fork_session(session_id, last)?;
run_interactive(&cli, &config, Some(new_session_id)).await
}
Commands::ResponsesApiProxy(args) => {
responses_api_proxy::run_main(args)?;
Ok(())
}
};
}
let config = load_config_from_cli(&cli)?;
if let Some(prompt) = cli.prompt {
let model = config.default_model();
return run_one_shot(&config, &model, &prompt).await;
}
let resume_session_id = if cli.continue_session {
match session_manager::SessionManager::default_location() {
Ok(manager) => manager.get_latest_session().ok().flatten().map(|m| m.id),
Err(_) => None,
}
} else {
cli.resume.clone()
};
run_interactive(&cli, &config, resume_session_id).await
}
fn generate_completions(shell: Shell) {
let mut cmd = Cli::command();
let name = cmd.get_name().to_string();
generate(shell, &mut cmd, name, &mut io::stdout());
}
fn run_eval(args: EvalArgs) -> Result<()> {
let fail_step = match args.fail_step.as_deref() {
Some(value) => ScenarioStepKind::parse(value)
.map(Some)
.ok_or_else(|| anyhow!("invalid --fail-step '{value}'"))?,
None => None,
};
let config = EvalHarnessConfig {
fail_step,
shell_command: args.shell_command,
shell_expect_token: args.shell_expect_token,
max_output_chars: args.max_output_chars,
record_dir: args.record.clone(),
..EvalHarnessConfig::default()
};
let harness = EvalHarness::new(config);
let run = harness.run().context("evaluation harness failed")?;
let report = run.to_report();
if args.json {
let json = serde_json::to_string_pretty(&report)?;
println!("{json}");
} else {
println!("Offline Eval Harness");
println!("scenario: {}", report.scenario_name);
println!("workspace: {}", report.workspace_root.display());
println!("success: {}", report.metrics.success);
println!("steps: {}", report.metrics.steps);
println!("tool_errors: {}", report.metrics.tool_errors);
println!("duration_ms: {}", report.metrics.duration.as_millis());
if !report.metrics.per_tool.is_empty() {
println!("per_tool:");
for (kind, stats) in &report.metrics.per_tool {
println!(
" {} invocations={} errors={} duration_ms={}",
kind.tool_name(),
stats.invocations,
stats.errors,
stats.total_duration.as_millis()
);
}
}
let failed_steps: Vec<_> = report.steps.iter().filter(|s| !s.success).collect();
if !failed_steps.is_empty() {
println!("failed_steps:");
for step in failed_steps {
let error = step.error.as_deref().unwrap_or("unknown error");
println!(
" {} tool={} error={}",
step.kind.tool_name(),
step.tool_name,
error
);
}
}
}
if report.metrics.success {
Ok(())
} else {
bail!("offline evaluation harness reported failure")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum WriteStatus {
Created,
Overwritten,
SkippedExists,
}
fn ensure_parent_dir(path: &Path) -> Result<()> {
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
{
std::fs::create_dir_all(parent)
.with_context(|| format!("Failed to create directory for {}", parent.display()))?;
}
Ok(())
}
fn write_template_file(path: &Path, contents: &str, force: bool) -> Result<WriteStatus> {
ensure_parent_dir(path)?;
if path.exists() && !force {
return Ok(WriteStatus::SkippedExists);
}
let status = if path.exists() {
WriteStatus::Overwritten
} else {
WriteStatus::Created
};
std::fs::write(path, contents)
.with_context(|| format!("Failed to write template at {}", path.display()))?;
Ok(status)
}
fn mcp_template_json() -> Result<String> {
let mut cfg = McpConfig::default();
cfg.servers.insert(
"example".to_string(),
McpServerConfig {
command: Some("node".to_string()),
args: vec!["./path/to/your-mcp-server.js".to_string()],
env: std::collections::HashMap::new(),
url: None,
connect_timeout: None,
execute_timeout: None,
read_timeout: None,
disabled: true,
enabled: true,
required: false,
enabled_tools: Vec::new(),
disabled_tools: Vec::new(),
},
);
serde_json::to_string_pretty(&cfg)
.map_err(|e| anyhow!("Failed to render MCP template JSON: {e}"))
}
fn init_mcp_config(path: &Path, force: bool) -> Result<WriteStatus> {
let template = mcp_template_json()?;
write_template_file(path, &template, force)
}
fn skills_template(name: &str) -> String {
format!(
"\
---\n\
name: {name}\n\
description: Quick repo diagnostics and setup guidance\n\
allowed-tools: diagnostics, list_dir, read_file, grep_files, git_status, git_diff\n\
---\n\n\
When this skill is active:\n\
1. Run the diagnostics tool to report workspace and sandbox status.\n\
2. Skim key project files (README.md, Cargo.toml, AGENTS.md) before editing.\n\
3. Prefer small, validated changes and summarize what you verified.\n\
"
)
}
fn init_skills_dir(skills_dir: &Path, force: bool) -> Result<(PathBuf, WriteStatus)> {
std::fs::create_dir_all(skills_dir)
.with_context(|| format!("Failed to create skills dir {}", skills_dir.display()))?;
let skill_name = "getting-started";
let skill_path = skills_dir.join(skill_name).join("SKILL.md");
ensure_parent_dir(&skill_path)?;
let status = write_template_file(&skill_path, &skills_template(skill_name), force)?;
Ok((skill_path, status))
}
fn tools_readme_template() -> &'static str {
"# Local tools\n\n\
Drop self-describing scripts here so they can be discovered by\n\
`deepseek-tui setup --status` and surfaced in `deepseek-tui doctor`.\n\n\
Each script should start with a frontmatter-style header so the\n\
description is visible without executing the file:\n\n\
```\n\
# name: my-tool\n\
# description: One-line summary of what this tool does\n\
# usage: my-tool [args...]\n\
```\n\n\
The directory is intentionally not auto-loaded into the agent's tool\n\
catalog. Wire individual tools through MCP, hooks, or skills when you\n\
want them available inside a session.\n"
}
fn tools_example_script() -> &'static str {
"#!/usr/bin/env sh\n\
# name: example\n\
# description: Print a confirmation that local tool discovery works\n\
# usage: example [name]\n\
printf 'deepseek-tui local tool ok: %s\\n' \"${1:-world}\"\n"
}
fn init_tools_dir(tools_dir: &Path, force: bool) -> Result<(PathBuf, WriteStatus, WriteStatus)> {
std::fs::create_dir_all(tools_dir)
.with_context(|| format!("Failed to create tools dir {}", tools_dir.display()))?;
let readme_path = tools_dir.join("README.md");
let readme_status = write_template_file(&readme_path, tools_readme_template(), force)?;
let example_path = tools_dir.join("example.sh");
let example_status = write_template_file(&example_path, tools_example_script(), force)?;
Ok((tools_dir.to_path_buf(), readme_status, example_status))
}
fn plugins_readme_template() -> &'static str {
"# Local plugins\n\n\
Plugins are richer than tools: each one lives in its own subdirectory\n\
with a `PLUGIN.md` describing what it does and how to enable it. The\n\
directory is created so users have a documented place to drop\n\
experiments without touching `~/.deepseek/skills/`.\n\n\
A plugin layout looks like:\n\n\
```\n\
plugins/\n\
my-plugin/\n\
PLUGIN.md # frontmatter + body, same shape as SKILL.md\n\
scripts/ # optional helpers invoked by the plugin\n\
```\n\n\
Plugins are not loaded automatically. Wire them up through skills,\n\
hooks, or MCP servers when you want them active in a session.\n"
}
fn plugin_example_template() -> &'static str {
"---\n\
name: example\n\
description: Placeholder plugin so /skills and doctor have something to show\n\
status: example\n\
---\n\n\
This is a starter plugin layout. Edit or replace it once you have a\n\
real plugin. The agent does not load this file directly; reference it\n\
from a skill or MCP wrapper if you want it active in a session.\n"
}
fn init_plugins_dir(
plugins_dir: &Path,
force: bool,
) -> Result<(PathBuf, PathBuf, WriteStatus, WriteStatus)> {
std::fs::create_dir_all(plugins_dir)
.with_context(|| format!("Failed to create plugins dir {}", plugins_dir.display()))?;
let readme_path = plugins_dir.join("README.md");
let readme_status = write_template_file(&readme_path, plugins_readme_template(), force)?;
let example_path = plugins_dir.join("example").join("PLUGIN.md");
ensure_parent_dir(&example_path)?;
let example_status = write_template_file(&example_path, plugin_example_template(), force)?;
Ok((readme_path, example_path, readme_status, example_status))
}
fn deepseek_home_dir() -> PathBuf {
dirs::home_dir().map_or_else(|| PathBuf::from(".deepseek"), |h| h.join(".deepseek"))
}
fn default_tools_dir() -> PathBuf {
deepseek_home_dir().join("tools")
}
fn default_plugins_dir() -> PathBuf {
deepseek_home_dir().join("plugins")
}
fn default_checkpoints_dir() -> PathBuf {
deepseek_home_dir().join("sessions").join("checkpoints")
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct CleanPlan {
targets: Vec<PathBuf>,
}
fn collect_clean_targets(checkpoints_dir: &Path) -> CleanPlan {
let candidates = ["latest.json", "offline_queue.json"];
let targets = candidates
.iter()
.map(|name| checkpoints_dir.join(name))
.filter(|p| p.exists())
.collect();
CleanPlan { targets }
}
fn execute_clean_plan(plan: &CleanPlan) -> Result<Vec<PathBuf>> {
let mut removed = Vec::with_capacity(plan.targets.len());
for path in &plan.targets {
std::fs::remove_file(path)
.with_context(|| format!("Failed to remove {}", path.display()))?;
removed.push(path.clone());
}
Ok(removed)
}
fn run_setup(config: &Config, workspace: &Path, args: SetupArgs) -> Result<()> {
if args.status {
return run_setup_status(config, workspace);
}
if args.clean {
return run_setup_clean(&default_checkpoints_dir(), args.force);
}
use crate::palette;
use colored::Colorize;
let (aqua_r, aqua_g, aqua_b) = palette::DEEPSEEK_SKY_RGB;
let (sky_r, sky_g, sky_b) = palette::DEEPSEEK_SKY_RGB;
let any_explicit = args.mcp || args.skills || args.tools || args.plugins;
let run_mcp = args.mcp || args.all || !any_explicit;
let run_skills = args.skills || args.all || !any_explicit;
let run_tools = args.tools || args.all;
let run_plugins = args.plugins || args.all;
println!(
"{}",
"DeepSeek Setup".truecolor(aqua_r, aqua_g, aqua_b).bold()
);
println!("{}", "==============".truecolor(sky_r, sky_g, sky_b));
println!("Workspace: {}", workspace.display());
if run_mcp {
let mcp_path = config.mcp_config_path();
let status = init_mcp_config(&mcp_path, args.force)?;
match status {
WriteStatus::Created => {
println!(" ✓ Created MCP config at {}", mcp_path.display());
}
WriteStatus::Overwritten => {
println!(" ✓ Overwrote MCP config at {}", mcp_path.display());
}
WriteStatus::SkippedExists => {
println!(" · MCP config already exists at {}", mcp_path.display());
}
}
println!(" Next: edit the file, then run `deepseek mcp list` or `deepseek mcp tools`.");
}
if run_skills {
let skills_dir = if args.local {
workspace.join("skills")
} else {
config.skills_dir()
};
let (skill_path, status) = init_skills_dir(&skills_dir, args.force)?;
match status {
WriteStatus::Created => {
println!(" ✓ Created example skill at {}", skill_path.display());
}
WriteStatus::Overwritten => {
println!(" ✓ Overwrote example skill at {}", skill_path.display());
}
WriteStatus::SkippedExists => {
println!(
" · Example skill already exists at {}",
skill_path.display()
);
}
}
if args.local {
println!(
" Local skills dir enabled for this workspace: {}",
skills_dir.display()
);
} else {
println!(" Skills dir: {}", skills_dir.display());
}
println!(" Next: run the TUI and use `/skills` then `/skill getting-started`.");
}
if run_tools {
let tools_dir = default_tools_dir();
let (dir, readme_status, example_status) = init_tools_dir(&tools_dir, args.force)?;
report_write_status("Tools README", &dir.join("README.md"), readme_status);
report_write_status("Example tool", &dir.join("example.sh"), example_status);
println!(" Tools dir: {}", dir.display());
println!(" Next: drop scripts here; surface them via skills/MCP when ready.");
}
if run_plugins {
let plugins_dir = default_plugins_dir();
let (readme_path, example_path, readme_status, example_status) =
init_plugins_dir(&plugins_dir, args.force)?;
report_write_status("Plugins README", &readme_path, readme_status);
report_write_status("Example plugin", &example_path, example_status);
println!(" Plugins dir: {}", plugins_dir.display());
println!(" Next: copy the example dir, edit PLUGIN.md, wire via skill/MCP.");
}
let sandbox = crate::sandbox::get_platform_sandbox();
if let Some(kind) = sandbox {
println!(" ✓ Sandbox available: {kind}");
} else {
println!(" · Sandbox not available on this platform (best-effort only).");
}
Ok(())
}
fn report_write_status(label: &str, path: &Path, status: WriteStatus) {
match status {
WriteStatus::Created => {
println!(" ✓ Created {label} at {}", path.display());
}
WriteStatus::Overwritten => {
println!(" ✓ Overwrote {label} at {}", path.display());
}
WriteStatus::SkippedExists => {
println!(" · {label} already exists at {}", path.display());
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ApiKeySource {
Env,
Config,
Missing,
}
fn resolve_api_key_source(config: &Config) -> ApiKeySource {
if std::env::var("DEEPSEEK_API_KEY")
.ok()
.filter(|k| !k.trim().is_empty())
.is_some()
{
ApiKeySource::Env
} else if config.deepseek_api_key().is_ok() {
ApiKeySource::Config
} else {
ApiKeySource::Missing
}
}
fn count_dir_entries(dir: &Path) -> usize {
std::fs::read_dir(dir)
.map(|entries| entries.filter_map(std::result::Result::ok).count())
.unwrap_or(0)
}
fn skills_count_for(dir: &Path) -> usize {
if !dir.exists() {
return 0;
}
crate::skills::SkillRegistry::discover(dir).len()
}
fn run_setup_status(config: &Config, workspace: &Path) -> Result<()> {
use crate::palette;
use colored::Colorize;
let (aqua_r, aqua_g, aqua_b) = palette::DEEPSEEK_SKY_RGB;
let (sky_r, sky_g, sky_b) = palette::DEEPSEEK_SKY_RGB;
let (red_r, red_g, red_b) = palette::DEEPSEEK_RED_RGB;
println!(
"{}",
"DeepSeek Status".truecolor(aqua_r, aqua_g, aqua_b).bold()
);
println!("{}", "===============".truecolor(sky_r, sky_g, sky_b));
println!("workspace: {}", workspace.display());
match resolve_api_key_source(config) {
ApiKeySource::Env => println!(
" {} api_key: set via DEEPSEEK_API_KEY",
"✓".truecolor(aqua_r, aqua_g, aqua_b)
),
ApiKeySource::Config => println!(
" {} api_key: set via config",
"✓".truecolor(aqua_r, aqua_g, aqua_b)
),
ApiKeySource::Missing => {
let (env_var, login_hint) = match config.api_provider() {
crate::config::ApiProvider::NvidiaNim => (
"NVIDIA_API_KEY",
"deepseek auth set --provider nvidia-nim --api-key \"...\"",
),
crate::config::ApiProvider::Openrouter => (
"OPENROUTER_API_KEY",
"deepseek auth set --provider openrouter --api-key \"...\"",
),
crate::config::ApiProvider::Novita => (
"NOVITA_API_KEY",
"deepseek auth set --provider novita --api-key \"...\"",
),
crate::config::ApiProvider::Fireworks => (
"FIREWORKS_API_KEY",
"deepseek auth set --provider fireworks --api-key \"...\"",
),
crate::config::ApiProvider::Sglang => (
"SGLANG_API_KEY",
"deepseek auth set --provider sglang --api-key \"...\"",
),
crate::config::ApiProvider::Deepseek => {
("DEEPSEEK_API_KEY", "deepseek login --api-key \"...\"")
}
};
println!(
" {} api_key: missing (set {env_var} or `[providers.{}].api_key` in ~/.deepseek/config.toml; or run `{login_hint}`)",
"✗".truecolor(red_r, red_g, red_b),
match config.api_provider() {
crate::config::ApiProvider::NvidiaNim => "nvidia_nim",
crate::config::ApiProvider::Openrouter => "openrouter",
crate::config::ApiProvider::Novita => "novita",
crate::config::ApiProvider::Fireworks => "fireworks",
crate::config::ApiProvider::Sglang => "sglang",
crate::config::ApiProvider::Deepseek => "deepseek",
}
);
}
}
println!(
" · base_url: {}",
config
.base_url
.as_deref()
.unwrap_or("https://api.deepseek.com")
);
let model = config
.default_text_model
.clone()
.unwrap_or_else(|| DEFAULT_TEXT_MODEL.to_string());
println!(" · default_text_model: {model}");
let mcp_path = config.mcp_config_path();
let mcp_count = match load_mcp_config(&mcp_path) {
Ok(cfg) => cfg.servers.len(),
Err(_) => 0,
};
let mcp_present = if mcp_path.exists() { "" } else { " (missing)" };
println!(
" · mcp servers: {mcp_count} at {}{mcp_present}",
mcp_path.display()
);
let skills_dir = config.skills_dir();
println!(
" · skills: {} at {}",
skills_count_for(&skills_dir),
skills_dir.display()
);
let tools_dir = default_tools_dir();
let tools_present = if tools_dir.exists() {
""
} else {
" (missing — run `setup --tools`)"
};
println!(
" · tools: {} entries at {}{tools_present}",
if tools_dir.exists() {
count_dir_entries(&tools_dir)
} else {
0
},
tools_dir.display()
);
let plugins_dir = default_plugins_dir();
let plugins_present = if plugins_dir.exists() {
""
} else {
" (missing — run `setup --plugins`)"
};
println!(
" · plugins: {} entries at {}{plugins_present}",
if plugins_dir.exists() {
count_dir_entries(&plugins_dir)
} else {
0
},
plugins_dir.display()
);
let sandbox = crate::sandbox::get_platform_sandbox();
match sandbox {
Some(kind) => println!(
" {} sandbox: {kind}",
"✓".truecolor(aqua_r, aqua_g, aqua_b)
),
None => println!(
" {} sandbox: unavailable (commands run best-effort)",
"!".truecolor(sky_r, sky_g, sky_b)
),
}
println!(" {} {}", "·".dimmed(), dotenv_status_line(workspace));
println!();
println!("Run `deepseek-tui doctor --json` for a machine-readable check.");
Ok(())
}
fn dotenv_status_line(workspace: &Path) -> String {
let dotenv = workspace.join(".env");
if dotenv.exists() {
return format!(".env present at {}", dotenv.display());
}
if workspace.join(".env.example").exists() {
return ".env not present in workspace (run `cp .env.example .env` and edit)".to_string();
}
".env not present in workspace".to_string()
}
fn run_setup_clean(checkpoints_dir: &Path, force: bool) -> Result<()> {
use colored::Colorize;
if !checkpoints_dir.exists() {
println!(
"Nothing to clean — checkpoints dir does not exist: {}",
checkpoints_dir.display()
);
return Ok(());
}
let plan = collect_clean_targets(checkpoints_dir);
if plan.targets.is_empty() {
println!(
"Nothing to clean — no checkpoint files in {}",
checkpoints_dir.display()
);
return Ok(());
}
if !force {
println!(
"Would remove {} checkpoint file(s) (use --force to apply):",
plan.targets.len()
);
for path in &plan.targets {
println!(" · {}", path.display());
}
return Ok(());
}
let removed = execute_clean_plan(&plan)?;
println!("{}", "Cleaned checkpoints:".bold());
for path in &removed {
println!(" ✓ {}", path.display());
}
Ok(())
}
async fn run_doctor(config: &Config, workspace: &Path, config_path_override: Option<&Path>) {
use crate::palette;
use colored::Colorize;
let (blue_r, blue_g, blue_b) = palette::DEEPSEEK_BLUE_RGB;
let (sky_r, sky_g, sky_b) = palette::DEEPSEEK_SKY_RGB;
let (aqua_r, aqua_g, aqua_b) = palette::DEEPSEEK_SKY_RGB;
let (red_r, red_g, red_b) = palette::DEEPSEEK_RED_RGB;
println!(
"{}",
"DeepSeek TUI Doctor"
.truecolor(blue_r, blue_g, blue_b)
.bold()
);
println!("{}", "==================".truecolor(sky_r, sky_g, sky_b));
println!();
println!("{}", "Version Information:".bold());
println!(" deepseek-tui: {}", env!("CARGO_PKG_VERSION"));
println!(" rust: {}", rustc_version());
println!();
println!("{}", "Configuration:".bold());
let default_config_dir =
dirs::home_dir().map_or_else(|| PathBuf::from(".deepseek"), |h| h.join(".deepseek"));
let config_path = config_path_override
.map(PathBuf::from)
.or_else(|| {
std::env::var("DEEPSEEK_CONFIG_PATH")
.ok()
.map(PathBuf::from)
})
.unwrap_or_else(|| default_config_dir.join("config.toml"));
if config_path.exists() {
println!(
" {} config.toml found at {}",
"✓".truecolor(aqua_r, aqua_g, aqua_b),
config_path.display()
);
} else {
println!(
" {} config.toml not found at {} (using defaults/env)",
"!".truecolor(sky_r, sky_g, sky_b),
config_path.display()
);
}
println!(" workspace: {}", workspace.display());
println!();
println!("{}", "API Keys:".bold());
let secrets = deepseek_secrets::Secrets::auto_detect();
println!(" · keyring backend: {}", secrets.backend_name());
for (slot, env_names) in [
("deepseek", &["DEEPSEEK_API_KEY"][..]),
("nvidia-nim", &["NVIDIA_API_KEY", "NVIDIA_NIM_API_KEY"][..]),
("openrouter", &["OPENROUTER_API_KEY"][..]),
("novita", &["NOVITA_API_KEY"][..]),
] {
let in_keyring = secrets
.get(slot)
.ok()
.flatten()
.is_some_and(|v| !v.trim().is_empty());
let in_env = env_names.iter().any(|n| {
std::env::var(n)
.ok()
.filter(|v| !v.trim().is_empty())
.is_some()
});
let icon = if in_keyring || in_env {
"✓".truecolor(aqua_r, aqua_g, aqua_b)
} else {
"·".dimmed()
};
println!(
" {} {slot}: keyring={}, env={}",
icon,
if in_keyring { "yes" } else { "no" },
if in_env { "yes" } else { "no" }
);
}
let has_api_key = if config.deepseek_api_key().is_ok() {
println!(
" {} active provider key resolved",
"✓".truecolor(aqua_r, aqua_g, aqua_b)
);
true
} else {
println!(
" {} active provider key not configured",
"✗".truecolor(red_r, red_g, red_b)
);
println!(" Run 'deepseek auth set --provider <name>' to save a key to the OS keyring.");
false
};
println!();
println!("{}", "API Connectivity:".bold());
if has_api_key {
print!(" {} Testing connection to DeepSeek API...", "·".dimmed());
use std::io::Write;
std::io::stdout().flush().ok();
match test_api_connectivity(config).await {
Ok(model) => {
println!(
"\r {} API connection successful (model: {})",
"✓".truecolor(aqua_r, aqua_g, aqua_b),
model
);
}
Err(e) => {
let error_msg = e.to_string();
println!(
"\r {} API connection failed",
"✗".truecolor(red_r, red_g, red_b)
);
if error_msg.contains("401") || error_msg.contains("Unauthorized") {
println!(" Invalid API key. Check your DEEPSEEK_API_KEY or config.toml");
} else if error_msg.contains("403") || error_msg.contains("Forbidden") {
println!(
" API key lacks permissions. Verify key is active at platform.deepseek.com"
);
} else if error_msg.contains("timeout") || error_msg.contains("Timeout") {
println!(" Connection timed out. Check your network connection");
} else if error_msg.contains("dns") || error_msg.contains("resolve") {
println!(" DNS resolution failed. Check your network connection");
} else if error_msg.contains("connect") {
println!(" Connection failed. Check firewall settings or try again");
} else {
println!(" Error: {}", error_msg);
}
}
}
} else {
println!(" {} Skipped (no API key configured)", "·".dimmed());
}
println!();
println!("{}", "MCP Servers:".bold());
let features = config.features();
if features.enabled(Feature::Mcp) {
println!(
" {} MCP feature flag enabled",
"✓".truecolor(aqua_r, aqua_g, aqua_b)
);
} else {
println!(
" {} MCP feature flag disabled",
"!".truecolor(sky_r, sky_g, sky_b)
);
}
let mcp_config_path = config.mcp_config_path();
if mcp_config_path.exists() {
println!(
" {} MCP config found at {}",
"✓".truecolor(aqua_r, aqua_g, aqua_b),
mcp_config_path.display()
);
match load_mcp_config(&mcp_config_path) {
Ok(cfg) if cfg.servers.is_empty() => {
println!(" {} 0 server(s) configured", "·".dimmed());
}
Ok(cfg) => {
println!(
" {} {} server(s) configured",
"·".dimmed(),
cfg.servers.len()
);
for (name, server) in &cfg.servers {
let status = doctor_check_mcp_server(server);
let icon = match status {
McpServerDoctorStatus::Ok(ref detail) => {
format!(
" {} {name}: {}",
"✓".truecolor(aqua_r, aqua_g, aqua_b),
detail
)
}
McpServerDoctorStatus::Warning(ref detail) => {
format!(
" {} {name}: {}",
"!".truecolor(sky_r, sky_g, sky_b),
detail
)
}
McpServerDoctorStatus::Error(ref detail) => {
format!(
" {} {name}: {}",
"✗".truecolor(red_r, red_g, red_b),
detail
)
}
};
println!("{icon}");
if !server.enabled {
println!(" (disabled)");
}
}
}
Err(err) => {
println!(
" {} MCP config parse error: {}",
"✗".truecolor(red_r, red_g, red_b),
err
);
}
}
} else {
println!(
" {} MCP config not found at {}",
"·".dimmed(),
mcp_config_path.display()
);
println!(" Run `deepseek mcp init` or `deepseek setup --mcp`.");
}
println!();
println!("{}", "Skills:".bold());
let global_skills_dir = config.skills_dir();
let agents_skills_dir = workspace.join(".agents").join("skills");
let local_skills_dir = workspace.join("skills");
let selected_skills_dir = if agents_skills_dir.exists() {
&agents_skills_dir
} else if local_skills_dir.exists() {
&local_skills_dir
} else {
&global_skills_dir
};
let describe_dir = |dir: &Path| -> usize {
std::fs::read_dir(dir)
.map(|entries| entries.filter_map(std::result::Result::ok).count())
.unwrap_or(0)
};
if local_skills_dir.exists() {
println!(
" {} local skills dir found at {} ({} items)",
"✓".truecolor(aqua_r, aqua_g, aqua_b),
local_skills_dir.display(),
describe_dir(&local_skills_dir)
);
} else {
println!(
" {} local skills dir not found at {}",
"·".dimmed(),
local_skills_dir.display()
);
}
if agents_skills_dir.exists() {
println!(
" {} .agents skills dir found at {} ({} items)",
"✓".truecolor(aqua_r, aqua_g, aqua_b),
agents_skills_dir.display(),
describe_dir(&agents_skills_dir)
);
} else {
println!(
" {} .agents skills dir not found at {}",
"·".dimmed(),
agents_skills_dir.display()
);
}
if global_skills_dir.exists() {
println!(
" {} global skills dir found at {} ({} items)",
"✓".truecolor(aqua_r, aqua_g, aqua_b),
global_skills_dir.display(),
describe_dir(&global_skills_dir)
);
} else {
println!(
" {} global skills dir not found at {}",
"·".dimmed(),
global_skills_dir.display()
);
}
println!(
" {} selected skills dir: {}",
"·".dimmed(),
selected_skills_dir.display()
);
if !agents_skills_dir.exists() && !local_skills_dir.exists() && !global_skills_dir.exists() {
println!(" Run `deepseek setup --skills` (or add --local for ./skills).");
}
println!();
println!("{}", "Tools:".bold());
let tools_dir = default_tools_dir();
if tools_dir.exists() {
let count = count_dir_entries(&tools_dir);
println!(
" {} tools dir found at {} ({} items)",
"✓".truecolor(aqua_r, aqua_g, aqua_b),
tools_dir.display(),
count
);
} else {
println!(
" {} tools dir not found at {}",
"·".dimmed(),
tools_dir.display()
);
println!(" Run `deepseek-tui setup --tools` to scaffold a starter dir.");
}
println!();
println!("{}", "Plugins:".bold());
let plugins_dir = default_plugins_dir();
if plugins_dir.exists() {
let count = count_dir_entries(&plugins_dir);
println!(
" {} plugins dir found at {} ({} items)",
"✓".truecolor(aqua_r, aqua_g, aqua_b),
plugins_dir.display(),
count
);
} else {
println!(
" {} plugins dir not found at {}",
"·".dimmed(),
plugins_dir.display()
);
println!(" Run `deepseek-tui setup --plugins` to scaffold a starter dir.");
}
println!();
println!("{}", "Platform:".bold());
println!(" OS: {}", std::env::consts::OS);
println!(" Arch: {}", std::env::consts::ARCH);
let sandbox = crate::sandbox::get_platform_sandbox();
if let Some(kind) = sandbox {
println!(
" {} sandbox available: {}",
"✓".truecolor(aqua_r, aqua_g, aqua_b),
kind
);
} else {
println!(
" {} sandbox not available (commands run best-effort)",
"!".truecolor(sky_r, sky_g, sky_b)
);
}
println!();
println!(
"{}",
"All checks complete!"
.truecolor(aqua_r, aqua_g, aqua_b)
.bold()
);
}
fn run_doctor_json(
config: &Config,
workspace: &Path,
config_path_override: Option<&Path>,
) -> Result<()> {
use serde_json::json;
let default_config_dir =
dirs::home_dir().map_or_else(|| PathBuf::from(".deepseek"), |h| h.join(".deepseek"));
let config_path = config_path_override
.map(PathBuf::from)
.or_else(|| {
std::env::var("DEEPSEEK_CONFIG_PATH")
.ok()
.map(PathBuf::from)
})
.unwrap_or_else(|| default_config_dir.join("config.toml"));
let api_key_state = match resolve_api_key_source(config) {
ApiKeySource::Env => "env",
ApiKeySource::Config => "config",
ApiKeySource::Missing => "missing",
};
let mcp_config_path = config.mcp_config_path();
let mcp_present = mcp_config_path.exists();
let mcp_summary = match load_mcp_config(&mcp_config_path) {
Ok(cfg) => {
let servers: Vec<serde_json::Value> = cfg
.servers
.iter()
.map(|(name, server)| {
let status = doctor_check_mcp_server(server);
let (kind, detail) = match &status {
McpServerDoctorStatus::Ok(d) => ("ok", d.clone()),
McpServerDoctorStatus::Warning(d) => ("warning", d.clone()),
McpServerDoctorStatus::Error(d) => ("error", d.clone()),
};
json!({
"name": name,
"enabled": server.enabled && !server.disabled,
"status": kind,
"detail": detail,
})
})
.collect();
json!({
"config_path": mcp_config_path.display().to_string(),
"present": mcp_present,
"servers": servers,
})
}
Err(err) => json!({
"config_path": mcp_config_path.display().to_string(),
"present": mcp_present,
"servers": [],
"error": err.to_string(),
}),
};
let global_skills_dir = config.skills_dir();
let agents_skills_dir = workspace.join(".agents").join("skills");
let local_skills_dir = workspace.join("skills");
let selected_skills_dir = if agents_skills_dir.exists() {
agents_skills_dir.clone()
} else if local_skills_dir.exists() {
local_skills_dir.clone()
} else {
global_skills_dir.clone()
};
let tools_dir = default_tools_dir();
let plugins_dir = default_plugins_dir();
let report = json!({
"version": env!("CARGO_PKG_VERSION"),
"config_path": config_path.display().to_string(),
"config_present": config_path.exists(),
"workspace": workspace.display().to_string(),
"api_key": {
"source": api_key_state,
},
"base_url": config
.base_url
.clone()
.unwrap_or_else(|| "https://api.deepseek.com".to_string()),
"default_text_model": config
.default_text_model
.clone()
.unwrap_or_else(|| DEFAULT_TEXT_MODEL.to_string()),
"mcp": mcp_summary,
"skills": {
"selected": selected_skills_dir.display().to_string(),
"global": {
"path": global_skills_dir.display().to_string(),
"present": global_skills_dir.exists(),
"count": skills_count_for(&global_skills_dir),
},
"agents": {
"path": agents_skills_dir.display().to_string(),
"present": agents_skills_dir.exists(),
"count": skills_count_for(&agents_skills_dir),
},
"local": {
"path": local_skills_dir.display().to_string(),
"present": local_skills_dir.exists(),
"count": skills_count_for(&local_skills_dir),
},
},
"tools": {
"path": tools_dir.display().to_string(),
"present": tools_dir.exists(),
"count": if tools_dir.exists() { count_dir_entries(&tools_dir) } else { 0 },
},
"plugins": {
"path": plugins_dir.display().to_string(),
"present": plugins_dir.exists(),
"count": if plugins_dir.exists() { count_dir_entries(&plugins_dir) } else { 0 },
},
"sandbox": match crate::sandbox::get_platform_sandbox() {
Some(kind) => json!({"available": true, "kind": kind.to_string()}),
None => json!({"available": false, "kind": null}),
},
"platform": {
"os": std::env::consts::OS,
"arch": std::env::consts::ARCH,
},
"api_connectivity": {
"checked": false,
"note": "Skipped in --json mode; run `deepseek-tui doctor` for a live check.",
},
});
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
fn run_execpolicy_command(command: ExecpolicyCommand) -> Result<()> {
match command.command {
ExecpolicySubcommand::Check(cmd) => cmd.run(),
}
}
fn run_features_command(config: &Config, command: FeaturesCli) -> Result<()> {
match command.command {
FeaturesSubcommand::List => run_features_list(config),
}
}
fn stage_str(stage: features::Stage) -> &'static str {
match stage {
features::Stage::Experimental => "experimental",
features::Stage::Beta => "beta",
features::Stage::Stable => "stable",
features::Stage::Deprecated => "deprecated",
features::Stage::Removed => "removed",
}
}
fn run_features_list(config: &Config) -> Result<()> {
let features = config.features();
println!("feature\tstage\tenabled");
for spec in features::FEATURES {
let enabled = features.enabled(spec.id);
println!("{}\t{}\t{enabled}", spec.key, stage_str(spec.stage));
}
Ok(())
}
async fn run_models(config: &Config, args: ModelsArgs) -> Result<()> {
use crate::client::DeepSeekClient;
let client = DeepSeekClient::new(config)?;
let mut models = client.list_models().await?;
models.sort_by(|a, b| a.id.cmp(&b.id));
if args.json {
println!("{}", serde_json::to_string_pretty(&models)?);
return Ok(());
}
if models.is_empty() {
println!("No models returned by the API.");
return Ok(());
}
let default_model = config.default_model();
println!("Available models (default: {default_model})");
for model in models {
let marker = if model.id == default_model { "*" } else { " " };
if let Some(owner) = model.owned_by {
println!("{marker} {} ({owner})", model.id);
} else {
println!("{marker} {}", model.id);
}
}
Ok(())
}
async fn test_api_connectivity(config: &Config) -> Result<String> {
use crate::client::DeepSeekClient;
use crate::models::{ContentBlock, Message, MessageRequest};
let client = DeepSeekClient::new(config)?;
let model = client.model().to_string();
let request = MessageRequest {
model: model.clone(),
messages: vec![Message {
role: "user".to_string(),
content: vec![ContentBlock::Text {
text: "hi".to_string(),
cache_control: None,
}],
}],
max_tokens: 1,
system: None,
tools: None,
tool_choice: None,
metadata: None,
thinking: None,
reasoning_effort: None,
stream: Some(false),
temperature: None,
top_p: None,
};
let timeout_duration = std::time::Duration::from_secs(15);
match tokio::time::timeout(timeout_duration, client.create_message(request)).await {
Ok(Ok(_response)) => Ok(model),
Ok(Err(e)) => Err(e),
Err(_) => anyhow::bail!("Request timeout after 15 seconds"),
}
}
fn rustc_version() -> String {
std::process::Command::new("rustc")
.arg("--version")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map_or_else(|| "unknown".to_string(), |s| s.trim().to_string())
}
fn list_sessions(limit: usize, search: Option<String>) -> Result<()> {
use crate::palette;
use colored::Colorize;
use session_manager::{SessionManager, format_session_line};
let (blue_r, blue_g, blue_b) = palette::DEEPSEEK_BLUE_RGB;
let (sky_r, sky_g, sky_b) = palette::DEEPSEEK_SKY_RGB;
let (aqua_r, aqua_g, aqua_b) = palette::DEEPSEEK_SKY_RGB;
let manager = SessionManager::default_location()?;
let sessions = if let Some(query) = search {
manager.search_sessions(&query)?
} else {
manager.list_sessions()?
};
if sessions.is_empty() {
println!("{}", "No sessions found.".truecolor(sky_r, sky_g, sky_b));
println!(
"Start a new session with: {}",
"deepseek".truecolor(blue_r, blue_g, blue_b)
);
return Ok(());
}
println!(
"{}",
"Saved Sessions".truecolor(blue_r, blue_g, blue_b).bold()
);
println!("{}", "==============".truecolor(sky_r, sky_g, sky_b));
println!();
for (i, session) in sessions.iter().take(limit).enumerate() {
let line = format_session_line(session);
if i == 0 {
println!(" {} {}", "*".truecolor(aqua_r, aqua_g, aqua_b), line);
} else {
println!(" {line}");
}
}
let total = sessions.len();
if total > limit {
println!();
println!(
" {} more session(s). Use --limit to show more.",
total - limit
);
}
println!();
println!(
"Resume with: {} {}",
"deepseek --resume".truecolor(blue_r, blue_g, blue_b),
"<session-id>".dimmed()
);
println!(
"Continue latest: {}",
"deepseek --continue".truecolor(blue_r, blue_g, blue_b)
);
Ok(())
}
fn init_project() -> Result<()> {
use crate::palette;
use colored::Colorize;
use project_context::create_default_agents_md;
let (sky_r, sky_g, sky_b) = palette::DEEPSEEK_SKY_RGB;
let (aqua_r, aqua_g, aqua_b) = palette::DEEPSEEK_SKY_RGB;
let (red_r, red_g, red_b) = palette::DEEPSEEK_RED_RGB;
let workspace = std::env::current_dir()?;
let agents_path = workspace.join("AGENTS.md");
if agents_path.exists() {
println!(
"{} AGENTS.md already exists at {}",
"!".truecolor(sky_r, sky_g, sky_b),
agents_path.display()
);
return Ok(());
}
match create_default_agents_md(&workspace) {
Ok(path) => {
println!(
"{} Created {}",
"✓".truecolor(aqua_r, aqua_g, aqua_b),
path.display()
);
println!();
println!("Edit this file to customize how the AI agent works with your project.");
println!("The instructions will be loaded automatically when you run deepseek.");
}
Err(e) => {
println!(
"{} Failed to create AGENTS.md: {}",
"✗".truecolor(red_r, red_g, red_b),
e
);
}
}
Ok(())
}
fn resolve_workspace(cli: &Cli) -> PathBuf {
cli.workspace
.clone()
.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")))
}
fn load_config_from_cli(cli: &Cli) -> Result<Config> {
let profile = cli
.profile
.clone()
.or_else(|| std::env::var("DEEPSEEK_PROFILE").ok());
let mut config = Config::load(cli.config.clone(), profile.as_deref())?;
cli.feature_toggles.apply(&mut config)?;
Ok(config)
}
fn read_api_key_from_stdin() -> Result<String> {
let mut stdin = io::stdin();
if stdin.is_terminal() {
bail!("No API key provided. Pass --api-key or pipe one via stdin.");
}
let mut buffer = String::new();
stdin.read_to_string(&mut buffer)?;
let api_key = buffer.trim().to_string();
if api_key.is_empty() {
bail!("No API key provided via stdin.");
}
Ok(api_key)
}
fn run_login(api_key: Option<String>) -> Result<()> {
let api_key = match api_key {
Some(key) => key,
None => read_api_key_from_stdin()?,
};
let path = config::save_api_key(&api_key)?;
println!("Saved API key to {}", path.display());
Ok(())
}
fn run_logout() -> Result<()> {
config::clear_api_key()?;
println!("Cleared saved API key.");
Ok(())
}
fn resolve_session_id(session_id: Option<String>, last: bool) -> Result<String> {
if last {
return Ok("latest".to_string());
}
if let Some(id) = session_id {
return Ok(id);
}
pick_session_id()
}
fn fork_session(session_id: Option<String>, last: bool) -> Result<String> {
let manager = SessionManager::default_location()?;
let saved = if last {
let Some(meta) = manager.get_latest_session()? else {
bail!("No saved sessions found.");
};
manager.load_session(&meta.id)?
} else {
let id = resolve_session_id(session_id, false)?;
manager.load_session_by_prefix(&id)?
};
let system_prompt = saved
.system_prompt
.as_ref()
.map(|text| SystemPrompt::Text(text.clone()));
let forked = create_saved_session(
&saved.messages,
&saved.metadata.model,
&saved.metadata.workspace,
saved.metadata.total_tokens,
system_prompt.as_ref(),
);
manager.save_session(&forked)?;
Ok(forked.metadata.id)
}
fn pick_session_id() -> Result<String> {
let manager = SessionManager::default_location()?;
let sessions = manager.list_sessions()?;
if sessions.is_empty() {
bail!("No saved sessions found.");
}
println!("Select a session to resume:");
for (idx, session) in sessions.iter().enumerate() {
println!(" {:>2}. {} ({})", idx + 1, session.title, session.id);
}
print!("Enter a number (or press Enter to cancel): ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim();
if input.is_empty() {
bail!("No session selected.");
}
let idx: usize = input
.parse()
.map_err(|_| anyhow::anyhow!("Invalid input"))?;
let session = sessions
.get(idx.saturating_sub(1))
.ok_or_else(|| anyhow::anyhow!("Selection out of range"))?;
Ok(session.id.clone())
}
async fn run_review(config: &Config, args: ReviewArgs) -> Result<()> {
use crate::client::DeepSeekClient;
let diff = collect_diff(&args)?;
if diff.trim().is_empty() {
bail!("No diff to review.");
}
let model = args
.model
.or_else(|| config.default_text_model.clone())
.unwrap_or_else(|| config.default_model());
let system = SystemPrompt::Text(
"You are a senior code reviewer. Focus on bugs, risks, behavioral regressions, and missing tests. \
Provide findings ordered by severity with file references, then open questions, then a brief summary."
.to_string(),
);
let user_prompt =
format!("Review the following diff and provide feedback:\n\n{diff}\n\nEnd of diff.");
let client = DeepSeekClient::new(config)?;
let request = MessageRequest {
model: model.clone(),
messages: vec![Message {
role: "user".to_string(),
content: vec![ContentBlock::Text {
text: user_prompt,
cache_control: None,
}],
}],
max_tokens: 4096,
system: Some(system),
tools: None,
tool_choice: None,
metadata: None,
thinking: None,
reasoning_effort: None,
stream: Some(false),
temperature: Some(0.2),
top_p: Some(0.9),
};
let response = client.create_message(request).await?;
let mut output = String::new();
for block in response.content {
if let ContentBlock::Text { text, .. } = block {
output.push_str(&text);
}
}
if args.json {
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"mode": "review",
"model": model,
"success": true,
"content": output
}))?
);
} else {
println!("{output}");
}
Ok(())
}
fn collect_diff(args: &ReviewArgs) -> Result<String> {
let mut cmd = Command::new("git");
cmd.arg("diff");
if args.staged {
cmd.arg("--cached");
}
if let Some(base) = &args.base {
cmd.arg(format!("{base}...HEAD"));
}
if let Some(path) = &args.path {
cmd.arg("--").arg(path);
}
let output = cmd
.output()
.map_err(|e| anyhow::anyhow!("Failed to run git diff. Is git installed? ({})", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("git diff failed: {}", stderr.trim());
}
let mut diff = String::from_utf8_lossy(&output.stdout).to_string();
if diff.len() > args.max_chars {
diff = crate::utils::truncate_with_ellipsis(&diff, args.max_chars, "\n...[truncated]\n");
}
Ok(diff)
}
fn run_apply(args: ApplyArgs) -> Result<()> {
let patch = if let Some(path) = args.patch_file {
std::fs::read_to_string(&path)
.map_err(|e| anyhow::anyhow!("Failed to read patch {}: {}", path.display(), e))?
} else {
read_patch_from_stdin()?
};
if patch.trim().is_empty() {
bail!("Patch is empty.");
}
let mut tmp = NamedTempFile::new()?;
tmp.write_all(patch.as_bytes())?;
let tmp_path = tmp.path().to_path_buf();
let output = Command::new("git")
.arg("apply")
.arg("--whitespace=nowarn")
.arg(&tmp_path)
.output()
.map_err(|e| anyhow::anyhow!("Failed to run git apply: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("git apply failed: {}", stderr.trim());
}
println!("Applied patch successfully.");
Ok(())
}
fn read_patch_from_stdin() -> Result<String> {
let mut stdin = io::stdin();
if stdin.is_terminal() {
bail!("No patch file provided and stdin is empty.");
}
let mut buffer = String::new();
stdin.read_to_string(&mut buffer)?;
Ok(buffer)
}
async fn run_mcp_command(config: &Config, command: McpCommand) -> Result<()> {
let config_path = config.mcp_config_path();
match command {
McpCommand::Init { force } => {
let status = init_mcp_config(&config_path, force)?;
match status {
WriteStatus::Created => {
println!("Created MCP config at {}", config_path.display());
}
WriteStatus::Overwritten => {
println!("Overwrote MCP config at {}", config_path.display());
}
WriteStatus::SkippedExists => {
println!(
"MCP config already exists at {} (use --force to overwrite)",
config_path.display()
);
}
}
println!("Edit the file, then run `deepseek mcp list` or `deepseek mcp tools`.");
Ok(())
}
McpCommand::List => {
let cfg = load_mcp_config(&config_path)?;
if cfg.servers.is_empty() {
println!("No MCP servers configured in {}", config_path.display());
return Ok(());
}
println!("MCP servers ({}):", cfg.servers.len());
for (name, server) in cfg.servers {
let status = if server.enabled && !server.disabled {
"enabled"
} else {
"disabled"
};
let args = if server.args.is_empty() {
"".to_string()
} else {
format!(" {}", server.args.join(" "))
};
let cmd_str = if let Some(cmd) = server.command {
format!("{cmd}{args}")
} else if let Some(url) = server.url {
url
} else {
"unknown".to_string()
};
let required = if server.required { " required" } else { "" };
println!(" - {name} [{status}{required}] {cmd_str}");
}
Ok(())
}
McpCommand::Connect { server } => {
let mut pool = McpPool::from_config_path(&config_path)?;
if let Some(name) = server {
pool.get_or_connect(&name).await?;
println!("Connected to MCP server: {name}");
} else {
let errors = pool.connect_all().await;
if errors.is_empty() {
println!("Connected to all configured MCP servers.");
} else {
for (name, err) in errors {
eprintln!("Failed to connect {name}: {err}");
}
}
}
Ok(())
}
McpCommand::Tools { server } => {
let mut pool = McpPool::from_config_path(&config_path)?;
if let Some(name) = server {
let conn = pool.get_or_connect(&name).await?;
if conn.tools().is_empty() {
println!("No tools found for MCP server: {name}");
} else {
println!("Tools for {name}:");
for tool in conn.tools() {
println!(
" - {}{}",
tool.name,
tool.description
.as_ref()
.map_or(String::new(), |d| format!(": {d}"))
);
}
}
} else {
let _ = pool.connect_all().await;
let tools = pool.all_tools();
if tools.is_empty() {
println!("No MCP tools discovered.");
} else {
println!("MCP tools:");
for (name, tool) in tools {
println!(
" - {}{}",
name,
tool.description
.as_ref()
.map_or(String::new(), |d| format!(": {d}"))
);
}
}
}
Ok(())
}
McpCommand::Add {
name,
command,
url,
args,
} => {
if command.is_none() && url.is_none() {
bail!("Provide either --command or --url for `mcp add`.");
}
let mut cfg = load_mcp_config(&config_path)?;
cfg.servers.insert(
name.clone(),
McpServerConfig {
command,
args,
env: std::collections::HashMap::new(),
url,
connect_timeout: None,
execute_timeout: None,
read_timeout: None,
disabled: false,
enabled: true,
required: false,
enabled_tools: Vec::new(),
disabled_tools: Vec::new(),
},
);
save_mcp_config(&config_path, &cfg)?;
println!("Added MCP server '{name}' in {}", config_path.display());
Ok(())
}
McpCommand::Remove { name } => {
let mut cfg = load_mcp_config(&config_path)?;
if cfg.servers.remove(&name).is_none() {
bail!("MCP server '{name}' not found");
}
save_mcp_config(&config_path, &cfg)?;
println!("Removed MCP server '{name}'");
Ok(())
}
McpCommand::Enable { name } => {
let mut cfg = load_mcp_config(&config_path)?;
let server = cfg
.servers
.get_mut(&name)
.ok_or_else(|| anyhow!("MCP server '{name}' not found"))?;
server.enabled = true;
server.disabled = false;
save_mcp_config(&config_path, &cfg)?;
println!("Enabled MCP server '{name}'");
Ok(())
}
McpCommand::Disable { name } => {
let mut cfg = load_mcp_config(&config_path)?;
let server = cfg
.servers
.get_mut(&name)
.ok_or_else(|| anyhow!("MCP server '{name}' not found"))?;
server.enabled = false;
server.disabled = true;
save_mcp_config(&config_path, &cfg)?;
println!("Disabled MCP server '{name}'");
Ok(())
}
McpCommand::Validate => {
let mut pool = McpPool::from_config_path(&config_path)?;
let errors = pool.connect_all().await;
if errors.is_empty() {
println!("MCP config is valid. All enabled servers connected.");
return Ok(());
}
eprintln!("MCP validation failed:");
for (name, err) in errors {
eprintln!(" - {name}: {err}");
}
bail!("one or more MCP servers failed validation");
}
McpCommand::AddSelf { name, workspace } => {
let exe_path = std::env::current_exe()
.map_err(|e| anyhow!("Cannot resolve current binary path: {e}"))?;
let exe_str = exe_path.to_string_lossy().to_string();
let mut args = vec!["serve".to_string(), "--mcp".to_string()];
if let Some(ref ws) = workspace {
args.push("--workspace".to_string());
args.push(ws.clone());
}
let mut cfg = load_mcp_config(&config_path)?;
if cfg.servers.contains_key(&name) {
bail!(
"MCP server '{name}' already exists in {}. Use `deepseek mcp remove {name}` first, or choose a different --name.",
config_path.display()
);
}
cfg.servers.insert(
name.clone(),
McpServerConfig {
command: Some(exe_str.clone()),
args,
env: std::collections::HashMap::new(),
url: None,
connect_timeout: None,
execute_timeout: None,
read_timeout: None,
disabled: false,
enabled: true,
required: false,
enabled_tools: Vec::new(),
disabled_tools: Vec::new(),
},
);
save_mcp_config(&config_path, &cfg)?;
println!(
"Registered DeepSeek as MCP server '{name}' in {}",
config_path.display()
);
println!(" command: {exe_str}");
println!(
" args: serve --mcp{}",
workspace.map_or(String::new(), |ws| format!(" --workspace {ws}"))
);
println!();
println!("Tip: Use `deepseek mcp validate` to test the connection.");
println!(" Use `deepseek serve --http` for the HTTP/SSE runtime API instead.");
Ok(())
}
}
}
fn load_mcp_config(path: &Path) -> Result<McpConfig> {
if !path.exists() {
return Ok(McpConfig::default());
}
let contents = std::fs::read_to_string(path)
.map_err(|e| anyhow::anyhow!("Failed to read MCP config {}: {}", path.display(), e))?;
let cfg: McpConfig = serde_json::from_str(&contents)
.map_err(|e| anyhow::anyhow!("Failed to parse MCP config: {e}"))?;
Ok(cfg)
}
#[derive(Debug)]
enum McpServerDoctorStatus {
Ok(String),
Warning(String),
Error(String),
}
fn doctor_check_mcp_server(server: &McpServerConfig) -> McpServerDoctorStatus {
if server.command.is_none() && server.url.is_none() {
return McpServerDoctorStatus::Error("no command or url configured".to_string());
}
if let Some(ref url) = server.url {
return McpServerDoctorStatus::Ok(format!("HTTP/SSE server at {url}"));
}
let cmd = server.command.as_deref().unwrap_or("");
if cmd.is_empty() {
return McpServerDoctorStatus::Error("empty command".to_string());
}
let cmd_path = Path::new(cmd);
let is_absolute = cmd_path.is_absolute() || cmd.starts_with('/');
if is_absolute && !cmd_path.exists() {
return McpServerDoctorStatus::Error(format!("command not found: {cmd}"));
}
let is_self_hosted = server
.args
.windows(2)
.any(|w| w[0] == "serve" && w[1] == "--mcp");
let args_str = server.args.join(" ");
if is_self_hosted {
if is_absolute {
McpServerDoctorStatus::Ok(format!("self-hosted MCP server ({cmd} {args_str})"))
} else {
McpServerDoctorStatus::Warning(format!(
"self-hosted MCP server uses relative command \"{cmd}\" — consider using an absolute path"
))
}
} else {
McpServerDoctorStatus::Ok(format!(
"stdio server ({cmd}{})",
if args_str.is_empty() {
String::new()
} else {
format!(" {args_str}")
}
))
}
}
fn save_mcp_config(path: &Path, cfg: &McpConfig) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).with_context(|| {
format!("Failed to create MCP config directory {}", parent.display())
})?;
}
let rendered = serde_json::to_string_pretty(cfg)
.map_err(|e| anyhow!("Failed to serialize MCP config: {e}"))?;
std::fs::write(path, rendered)
.map_err(|e| anyhow!("Failed to write MCP config {}: {}", path.display(), e))?;
Ok(())
}
fn run_sandbox_command(args: SandboxArgs) -> Result<()> {
use crate::sandbox::{CommandSpec, SandboxManager};
let SandboxCommand::Run {
policy,
network,
writable_root,
exclude_tmpdir,
exclude_slash_tmp,
cwd,
timeout_ms,
command,
} = args.command;
let policy = parse_sandbox_policy(
&policy,
network,
writable_root,
exclude_tmpdir,
exclude_slash_tmp,
)?;
let cwd = cwd.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
let timeout = Duration::from_millis(timeout_ms.clamp(1000, 600_000));
let (program, args) = command
.split_first()
.ok_or_else(|| anyhow::anyhow!("Command is required"))?;
let spec =
CommandSpec::program(program, args.to_vec(), cwd.clone(), timeout).with_policy(policy);
let manager = SandboxManager::new();
let exec_env = manager.prepare(&spec);
let mut cmd = Command::new(exec_env.program());
cmd.args(exec_env.args())
.current_dir(&exec_env.cwd)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
for (key, value) in &exec_env.env {
cmd.env(key, value);
}
let mut child = cmd
.spawn()
.map_err(|e| anyhow::anyhow!("Failed to run command: {e}"))?;
let stdout_handle = child
.stdout
.take()
.ok_or_else(|| anyhow::anyhow!("stdout unavailable"))?;
let stderr_handle = child
.stderr
.take()
.ok_or_else(|| anyhow::anyhow!("stderr unavailable"))?;
let timeout = exec_env.timeout;
let stdout_thread = std::thread::spawn(move || {
let mut reader = stdout_handle;
let mut buf = Vec::new();
let _ = reader.read_to_end(&mut buf);
buf
});
let stderr_thread = std::thread::spawn(move || {
let mut reader = stderr_handle;
let mut buf = Vec::new();
let _ = reader.read_to_end(&mut buf);
buf
});
if let Some(status) = child.wait_timeout(timeout)? {
let stdout = stdout_thread.join().unwrap_or_default();
let stderr = stderr_thread.join().unwrap_or_default();
let stderr_str = String::from_utf8_lossy(&stderr);
let exit_code = status.code().unwrap_or(-1);
let sandbox_type = exec_env.sandbox_type;
let sandbox_denied = SandboxManager::was_denied(sandbox_type, exit_code, &stderr_str);
if !stdout.is_empty() {
print!("{}", String::from_utf8_lossy(&stdout));
}
if !stderr.is_empty() {
eprint!("{}", stderr_str);
}
if sandbox_denied {
eprintln!(
"{}",
SandboxManager::denial_message(sandbox_type, &stderr_str)
);
}
if !status.success() {
bail!("Command failed with exit code {exit_code}");
}
} else {
let _ = child.kill();
let _ = child.wait();
bail!("Command timed out after {}ms", timeout.as_millis());
}
Ok(())
}
fn parse_sandbox_policy(
policy: &str,
network: bool,
writable_root: Vec<PathBuf>,
exclude_tmpdir: bool,
exclude_slash_tmp: bool,
) -> Result<crate::sandbox::SandboxPolicy> {
use crate::sandbox::SandboxPolicy;
match policy {
"danger-full-access" => Ok(SandboxPolicy::DangerFullAccess),
"read-only" => Ok(SandboxPolicy::ReadOnly),
"external-sandbox" => Ok(SandboxPolicy::ExternalSandbox {
network_access: network,
}),
"workspace-write" => Ok(SandboxPolicy::WorkspaceWrite {
writable_roots: writable_root,
network_access: network,
exclude_tmpdir,
exclude_slash_tmp,
}),
other => bail!("Unknown sandbox policy: {other}"),
}
}
fn should_use_alt_screen(cli: &Cli, config: &Config) -> bool {
if cli.no_alt_screen {
return false;
}
let mode = config
.tui
.as_ref()
.and_then(|tui| tui.alternate_screen.as_deref())
.unwrap_or("auto")
.to_ascii_lowercase();
match mode.as_str() {
"always" => true,
"never" => false,
_ => !is_zellij(),
}
}
fn should_use_mouse_capture(cli: &Cli, config: &Config, use_alt_screen: bool) -> bool {
if !use_alt_screen || cli.no_mouse_capture {
return false;
}
if cli.mouse_capture {
return true;
}
config
.tui
.as_ref()
.and_then(|tui| tui.mouse_capture)
.unwrap_or(true)
}
fn is_zellij() -> bool {
std::env::var_os("ZELLIJ").is_some()
}
async fn run_interactive(
cli: &Cli,
config: &Config,
resume_session_id: Option<String>,
) -> Result<()> {
let workspace = cli
.workspace
.clone()
.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
let model = config.default_model();
let max_subagents = cli.max_subagents.map_or_else(
|| config.max_subagents(),
|value| value.clamp(1, MAX_SUBAGENTS),
);
let use_alt_screen = should_use_alt_screen(cli, config);
let use_mouse_capture = should_use_mouse_capture(cli, config, use_alt_screen);
let use_bracketed_paste = crate::settings::Settings::load()
.map(|s| s.bracketed_paste)
.unwrap_or(true);
let skills_dir = config.skills_dir();
if let Err(e) = crate::skills::install_system_skills(&skills_dir) {
logging::warn(format!("Failed to install system skills: {e}"));
}
let snapshots = config.snapshots_config();
if snapshots.enabled {
session_manager::prune_workspace_snapshots(&workspace, snapshots.max_age());
}
tui::run_tui(
config,
tui::TuiOptions {
model,
workspace,
allow_shell: cli.yolo || config.allow_shell(),
use_alt_screen,
use_mouse_capture,
use_bracketed_paste,
skills_dir,
memory_path: config.memory_path(),
notes_path: config.notes_path(),
mcp_config_path: config.mcp_config_path(),
use_memory: false,
start_in_agent_mode: cli.yolo,
skip_onboarding: cli.skip_onboarding,
yolo: cli.yolo, resume_session_id,
max_subagents,
},
)
.await
}
async fn run_one_shot(config: &Config, model: &str, prompt: &str) -> Result<()> {
use crate::client::DeepSeekClient;
use crate::models::{ContentBlock, Message, MessageRequest};
let client = DeepSeekClient::new(config)?;
let request = MessageRequest {
model: model.to_string(),
messages: vec![Message {
role: "user".to_string(),
content: vec![ContentBlock::Text {
text: prompt.to_string(),
cache_control: None,
}],
}],
max_tokens: 4096,
system: None,
tools: None,
tool_choice: None,
metadata: None,
thinking: None,
reasoning_effort: None,
stream: Some(false),
temperature: None,
top_p: None,
};
let response = client.create_message(request).await?;
for block in response.content {
if let ContentBlock::Text { text, .. } = block {
println!("{text}");
}
}
Ok(())
}
async fn run_one_shot_json(config: &Config, model: &str, prompt: &str) -> Result<()> {
use crate::client::DeepSeekClient;
use crate::models::{ContentBlock, Message, MessageRequest, SystemPrompt};
let client = DeepSeekClient::new(config)?;
let request = MessageRequest {
model: model.to_string(),
messages: vec![Message {
role: "user".to_string(),
content: vec![ContentBlock::Text {
text: prompt.to_string(),
cache_control: None,
}],
}],
max_tokens: 4096,
system: Some(SystemPrompt::Text(
"You are a coding assistant. Give concise, actionable responses.".to_string(),
)),
tools: None,
tool_choice: None,
metadata: None,
thinking: None,
reasoning_effort: None,
stream: Some(false),
temperature: Some(0.2),
top_p: Some(0.9),
};
let response = client.create_message(request).await?;
let mut output = String::new();
for block in response.content {
if let ContentBlock::Text { text, .. } = block {
output.push_str(&text);
}
}
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"mode": "one-shot",
"model": model,
"success": true,
"output": output
}))?
);
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn run_exec_agent(
config: &Config,
model: &str,
prompt: &str,
workspace: PathBuf,
max_subagents: usize,
auto_approve: bool,
trust_mode: bool,
json_output: bool,
) -> Result<()> {
use crate::compaction::CompactionConfig;
use crate::core::engine::{EngineConfig, spawn_engine};
use crate::core::events::Event;
use crate::core::ops::Op;
use crate::models::{compaction_message_threshold_for_model, compaction_threshold_for_model};
use crate::tools::plan::new_shared_plan_state;
use crate::tools::todo::new_shared_todo_list;
use crate::tui::app::AppMode;
let compaction = CompactionConfig {
enabled: false,
model: model.to_string(),
token_threshold: compaction_threshold_for_model(model),
message_threshold: compaction_message_threshold_for_model(model),
..Default::default()
};
let network_policy = config.network.clone().map(|toml_cfg| {
crate::network_policy::NetworkPolicyDecider::with_default_audit(toml_cfg.into_runtime())
});
let lsp_config = config
.lsp
.clone()
.map(crate::config::LspConfigToml::into_runtime);
let engine_config = EngineConfig {
model: model.to_string(),
workspace: workspace.clone(),
allow_shell: auto_approve || config.allow_shell(),
trust_mode,
notes_path: config.notes_path(),
mcp_config_path: config.mcp_config_path(),
max_steps: 100,
max_subagents,
features: config.features(),
compaction,
cycle: crate::cycle_manager::CycleConfig::default(),
capacity: crate::core::capacity::CapacityControllerConfig::from_app_config(config),
todos: new_shared_todo_list(),
plan_state: new_shared_plan_state(),
max_spawn_depth: crate::tools::subagent::DEFAULT_MAX_SPAWN_DEPTH,
network_policy,
snapshots_enabled: config.snapshots_config().enabled,
lsp_config,
};
let engine_handle = spawn_engine(engine_config, config);
let mode = if auto_approve {
AppMode::Yolo
} else {
AppMode::Agent
};
engine_handle
.send(Op::send(
prompt,
mode,
model,
None,
auto_approve || config.allow_shell(),
trust_mode,
auto_approve,
))
.await?;
#[derive(serde::Serialize)]
struct ExecToolEntry {
name: String,
success: bool,
output: String,
}
#[derive(serde::Serialize, Default)]
struct ExecSummary {
mode: String,
model: String,
prompt: String,
output: String,
tools: Vec<ExecToolEntry>,
status: Option<String>,
error: Option<String>,
}
let mut summary = ExecSummary {
mode: "agent".to_string(),
model: model.to_string(),
prompt: prompt.to_string(),
..ExecSummary::default()
};
let mut stdout = io::stdout();
let mut ends_with_newline = false;
loop {
let event = {
let mut rx = engine_handle.rx_event.write().await;
rx.recv().await
};
let Some(event) = event else {
break;
};
match event {
Event::MessageDelta { content, .. } => {
summary.output.push_str(&content);
if !json_output {
print!("{content}");
stdout.flush()?;
}
ends_with_newline = content.ends_with('\n');
}
Event::MessageComplete { .. } if !json_output && !ends_with_newline => {
println!();
}
Event::ToolCallStarted { name, input, .. } if !json_output => {
let summary = summarize_tool_args(&input);
if let Some(summary) = summary {
eprintln!("tool: {name} ({summary})");
} else {
eprintln!("tool: {name}");
}
}
Event::ToolCallProgress { id, output } if !json_output => {
eprintln!("tool {id}: {}", summarize_tool_output(&output));
}
Event::ToolCallComplete { name, result, .. } => match result {
Ok(output) => {
summary.tools.push(ExecToolEntry {
name: name.clone(),
success: output.success,
output: output.content.clone(),
});
if name == "exec_shell" && !output.content.trim().is_empty() {
if !json_output {
eprintln!("tool {name} completed");
eprintln!(
"--- stdout/stderr ---\n{}\n---------------------",
output.content
);
}
} else if !json_output {
eprintln!(
"tool {name} completed: {}",
summarize_tool_output(&output.content)
);
}
}
Err(err) => {
summary.tools.push(ExecToolEntry {
name: name.clone(),
success: false,
output: err.to_string(),
});
if !json_output {
eprintln!("tool {name} failed: {err}");
}
}
},
Event::AgentSpawned { id, prompt } => {
eprintln!("sub-agent {id} spawned: {}", summarize_tool_output(&prompt));
}
Event::AgentProgress { id, status } => {
eprintln!("sub-agent {id}: {status}");
}
Event::AgentComplete { id, result } => {
eprintln!(
"sub-agent {id} completed: {}",
summarize_tool_output(&result)
);
}
Event::ApprovalRequired { id, .. } => {
if auto_approve {
let _ = engine_handle.approve_tool_call(id).await;
} else {
let _ = engine_handle.deny_tool_call(id).await;
}
}
Event::ElevationRequired {
tool_id,
tool_name,
denial_reason,
..
} => {
if auto_approve {
eprintln!("sandbox denied {tool_name}: {denial_reason} (auto-elevating)");
let policy = crate::sandbox::SandboxPolicy::DangerFullAccess;
let _ = engine_handle.retry_tool_with_policy(tool_id, policy).await;
} else {
eprintln!("sandbox denied {tool_name}: {denial_reason}");
let _ = engine_handle.deny_tool_call(tool_id).await;
}
}
Event::Error {
envelope,
recoverable: _,
} => {
summary.error = Some(envelope.message.clone());
if !json_output {
eprintln!("error: {}", envelope.message);
}
}
Event::TurnComplete { status, error, .. } => {
summary.status = Some(format!("{status:?}").to_lowercase());
summary.error = error;
let _ = engine_handle.send(Op::Shutdown).await;
break;
}
_ => {}
}
}
if json_output {
println!("{}", serde_json::to_string_pretty(&summary)?);
}
Ok(())
}
#[cfg(test)]
mod terminal_mode_tests {
use super::*;
use clap::Parser;
fn parse_cli(args: &[&str]) -> Cli {
Cli::try_parse_from(args).expect("CLI args should parse")
}
#[test]
fn mouse_capture_defaults_on_when_alternate_screen_is_active() {
let cli = parse_cli(&["deepseek"]);
let config = Config::default();
assert!(should_use_mouse_capture(&cli, &config, true));
}
#[test]
fn no_mouse_capture_flag_disables_mouse_capture() {
let cli = parse_cli(&["deepseek", "--no-mouse-capture"]);
let config = Config::default();
assert!(!should_use_mouse_capture(&cli, &config, true));
}
#[test]
fn config_can_disable_default_mouse_capture() {
let cli = parse_cli(&["deepseek"]);
let config = Config {
tui: Some(crate::config::TuiConfig {
alternate_screen: None,
mouse_capture: Some(false),
status_items: None,
}),
..Config::default()
};
assert!(!should_use_mouse_capture(&cli, &config, true));
}
#[test]
fn mouse_capture_is_off_without_alternate_screen() {
let cli = parse_cli(&["deepseek", "--mouse-capture"]);
let config = Config::default();
assert!(!should_use_mouse_capture(&cli, &config, false));
}
}
#[cfg(test)]
mod doctor_mcp_tests {
use super::*;
fn make_server(command: Option<&str>, args: &[&str], url: Option<&str>) -> McpServerConfig {
McpServerConfig {
command: command.map(String::from),
args: args.iter().map(|s| s.to_string()).collect(),
env: std::collections::HashMap::new(),
url: url.map(String::from),
connect_timeout: None,
execute_timeout: None,
read_timeout: None,
disabled: false,
enabled: true,
required: false,
enabled_tools: Vec::new(),
disabled_tools: Vec::new(),
}
}
#[test]
fn test_no_command_or_url_is_error() {
let server = make_server(None, &[], None);
assert!(matches!(
doctor_check_mcp_server(&server),
McpServerDoctorStatus::Error(_)
));
}
#[test]
fn test_url_server_is_ok() {
let server = make_server(None, &[], Some("http://localhost:3000/mcp"));
match doctor_check_mcp_server(&server) {
McpServerDoctorStatus::Ok(detail) => assert!(detail.contains("HTTP/SSE")),
other => panic!("Expected Ok, got {other:?}"),
}
}
#[test]
fn test_command_server_is_ok() {
let server = make_server(Some("node"), &["server.js"], None);
match doctor_check_mcp_server(&server) {
McpServerDoctorStatus::Ok(detail) => assert!(detail.contains("stdio")),
other => panic!("Expected Ok, got {other:?}"),
}
}
#[test]
fn test_self_hosted_absolute_is_ok() {
let server = make_server(Some("/usr/local/bin/deepseek"), &["serve", "--mcp"], None);
match doctor_check_mcp_server(&server) {
McpServerDoctorStatus::Ok(detail) | McpServerDoctorStatus::Error(detail) => {
assert!(
detail.contains("self-hosted") || detail.contains("not found"),
"unexpected detail: {detail}"
);
}
McpServerDoctorStatus::Warning(detail) => {
panic!("Absolute path should not warn: {detail}")
}
}
}
#[test]
fn test_self_hosted_relative_is_warning() {
let server = make_server(Some("deepseek"), &["serve", "--mcp"], None);
match doctor_check_mcp_server(&server) {
McpServerDoctorStatus::Warning(detail) => {
assert!(detail.contains("relative"));
}
other => panic!("Expected Warning for relative path, got {other:?}"),
}
}
#[test]
fn test_empty_command_is_error() {
let server = make_server(Some(""), &[], None);
assert!(matches!(
doctor_check_mcp_server(&server),
McpServerDoctorStatus::Error(_)
));
}
}
#[cfg(test)]
mod setup_helper_tests {
use super::*;
use std::collections::BTreeSet;
use tempfile::TempDir;
#[test]
fn init_tools_dir_creates_readme_and_example() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path().join("tools");
let (returned_dir, readme_status, example_status) =
init_tools_dir(&dir, false).expect("init_tools_dir should succeed");
assert_eq!(returned_dir, dir);
assert!(matches!(readme_status, WriteStatus::Created));
assert!(matches!(example_status, WriteStatus::Created));
assert!(dir.join("README.md").exists());
assert!(dir.join("example.sh").exists());
let readme = std::fs::read_to_string(dir.join("README.md")).unwrap();
assert!(
readme.contains("# name:"),
"README must show frontmatter convention"
);
let example = std::fs::read_to_string(dir.join("example.sh")).unwrap();
assert!(example.starts_with("#!/usr/bin/env sh"));
assert!(example.contains("# name: example"));
assert!(example.contains("# description:"));
}
#[test]
fn init_tools_dir_skips_existing_without_force() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path().join("tools");
let _ = init_tools_dir(&dir, false).unwrap();
let (_, readme_status, example_status) = init_tools_dir(&dir, false).unwrap();
assert!(matches!(readme_status, WriteStatus::SkippedExists));
assert!(matches!(example_status, WriteStatus::SkippedExists));
}
#[test]
fn init_tools_dir_force_overwrites() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path().join("tools");
let _ = init_tools_dir(&dir, false).unwrap();
std::fs::write(dir.join("example.sh"), "stale").unwrap();
let (_, _, example_status) = init_tools_dir(&dir, true).unwrap();
assert!(matches!(example_status, WriteStatus::Overwritten));
let example = std::fs::read_to_string(dir.join("example.sh")).unwrap();
assert_ne!(example, "stale");
}
#[test]
fn init_plugins_dir_creates_readme_and_example_layout() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path().join("plugins");
let (readme_path, example_path, readme_status, example_status) =
init_plugins_dir(&dir, false).unwrap();
assert_eq!(readme_path, dir.join("README.md"));
assert_eq!(example_path, dir.join("example").join("PLUGIN.md"));
assert!(matches!(readme_status, WriteStatus::Created));
assert!(matches!(example_status, WriteStatus::Created));
assert!(readme_path.exists());
assert!(example_path.exists());
let plugin_md = std::fs::read_to_string(&example_path).unwrap();
assert!(plugin_md.contains("---"));
assert!(plugin_md.contains("name: example"));
}
#[test]
fn collect_clean_targets_finds_only_known_files() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
std::fs::write(dir.join("latest.json"), "{}").unwrap();
std::fs::write(dir.join("offline_queue.json"), "[]").unwrap();
std::fs::write(dir.join("unrelated.json"), "{}").unwrap();
let plan = collect_clean_targets(dir);
assert_eq!(plan.targets.len(), 2);
assert!(plan.targets.iter().any(|p| p.ends_with("latest.json")));
assert!(
plan.targets
.iter()
.any(|p| p.ends_with("offline_queue.json"))
);
assert!(!plan.targets.iter().any(|p| p.ends_with("unrelated.json")));
}
#[test]
fn execute_clean_plan_removes_files_and_returns_them() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
let latest = dir.join("latest.json");
let queue = dir.join("offline_queue.json");
std::fs::write(&latest, "{}").unwrap();
std::fs::write(&queue, "[]").unwrap();
let plan = collect_clean_targets(dir);
let removed = execute_clean_plan(&plan).unwrap();
assert_eq!(removed.len(), 2);
assert!(!latest.exists());
assert!(!queue.exists());
}
#[test]
fn run_setup_clean_dry_run_lists_targets_without_force() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
std::fs::write(dir.join("latest.json"), "{}").unwrap();
run_setup_clean(dir, false).unwrap();
assert!(dir.join("latest.json").exists());
}
#[test]
fn run_setup_clean_force_removes_files() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
std::fs::write(dir.join("latest.json"), "{}").unwrap();
std::fs::write(dir.join("offline_queue.json"), "[]").unwrap();
run_setup_clean(dir, true).unwrap();
assert!(!dir.join("latest.json").exists());
assert!(!dir.join("offline_queue.json").exists());
}
#[test]
fn run_setup_clean_handles_missing_dir() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path().join("does-not-exist");
run_setup_clean(&dir, true).unwrap();
assert!(!dir.exists());
}
#[test]
fn dotenv_status_points_to_example_when_present() {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join(".env.example"), "DEEPSEEK_API_KEY=\n").unwrap();
assert_eq!(
dotenv_status_line(tmp.path()),
".env not present in workspace (run `cp .env.example .env` and edit)"
);
std::fs::write(tmp.path().join(".env"), "DEEPSEEK_API_KEY=test\n").unwrap();
assert!(dotenv_status_line(tmp.path()).contains(".env present at"));
}
#[test]
fn env_example_is_trackable_and_every_key_is_wired() {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let env_example = std::fs::read_to_string(root.join(".env.example")).unwrap();
let gitignore = std::fs::read_to_string(root.join(".gitignore")).unwrap();
assert!(gitignore.contains("!.env.example"));
let keys = documented_env_keys(&env_example);
for required in [
"DEEPSEEK_API_KEY",
"DEEPSEEK_BASE_URL",
"DEEPSEEK_MODEL",
"NVIDIA_API_KEY",
"NIM_BASE_URL",
"RUST_LOG",
"DEEPSEEK_APPROVAL_POLICY",
"DEEPSEEK_SANDBOX_MODE",
] {
assert!(
keys.contains(required),
".env.example is missing {required}"
);
}
let sources = [
include_str!("config.rs"),
include_str!("logging.rs"),
include_str!("../../config/src/lib.rs"),
include_str!("../../cli/src/main.rs"),
]
.join("\n");
for key in keys {
assert!(
sources.contains(&key),
".env.example documents {key}, but no source file references it"
);
}
}
fn documented_env_keys(content: &str) -> BTreeSet<String> {
content
.lines()
.filter_map(|line| {
let trimmed = line.trim();
let uncommented = trimmed
.strip_prefix('#')
.map(str::trim_start)
.unwrap_or(trimmed);
let (key, _) = uncommented.split_once('=')?;
let key = key.trim();
let is_env_key = key
.chars()
.all(|ch| ch.is_ascii_uppercase() || ch.is_ascii_digit() || ch == '_')
&& key.chars().any(|ch| ch == '_');
is_env_key.then(|| key.to_string())
})
.collect()
}
#[test]
fn resolve_api_key_source_reports_env_when_set() {
let prev = std::env::var("DEEPSEEK_API_KEY").ok();
unsafe {
std::env::set_var("DEEPSEEK_API_KEY", "test-helper-value");
}
let cfg = Config::default();
let source = resolve_api_key_source(&cfg);
match prev {
Some(value) => unsafe { std::env::set_var("DEEPSEEK_API_KEY", value) },
None => unsafe { std::env::remove_var("DEEPSEEK_API_KEY") },
}
assert_eq!(source, ApiKeySource::Env);
}
#[test]
fn skills_count_for_returns_zero_for_missing_dir() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path().join("nope");
assert_eq!(skills_count_for(&dir), 0);
}
#[test]
fn skills_count_for_counts_valid_skill_dirs() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path().join("skills");
let skill_dir = dir.join("getting-started");
std::fs::create_dir_all(&skill_dir).unwrap();
std::fs::write(
skill_dir.join("SKILL.md"),
"---\nname: getting-started\ndescription: hi\n---\nbody",
)
.unwrap();
assert_eq!(skills_count_for(&dir), 1);
}
}