release: v0.3.1

This commit is contained in:
Hunter Bown
2026-01-27 01:04:48 -06:00
parent 3204f556af
commit a5c02c0eb4
7 changed files with 399 additions and 74 deletions
+14 -1
View File
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.3.1] - 2026-01-27
### Added
- `deepseek setup` to bootstrap MCP config and skills directories
- `deepseek mcp init` to generate a template `mcp.json` at the configured path
### Changed
- `deepseek doctor` now follows the resolved config path and config-derived MCP/skills locations
### Fixed
- Doctor no longer reports missing MCP/skills when paths are overridden via config or env
## [0.3.0] - 2026-01-27 ## [0.3.0] - 2026-01-27
### Added ### Added
@@ -127,7 +139,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Hooks system and config profiles - Hooks system and config profiles
- Example skills and launch assets - Example skills and launch assets
[Unreleased]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.3.0...HEAD [Unreleased]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.3.1...HEAD
[0.3.1]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.2.2...v0.3.0 [0.3.0]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.2.2...v0.3.0
[0.2.2]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.2.1...v0.2.2 [0.2.2]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.2.1...v0.2.2
[0.2.1]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.2.0...v0.2.1 [0.2.1]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.2.0...v0.2.1
Generated
+1 -1
View File
@@ -646,7 +646,7 @@ dependencies = [
[[package]] [[package]]
name = "deepseek-tui" name = "deepseek-tui"
version = "0.3.0" version = "0.3.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arboard", "arboard",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "deepseek-tui" name = "deepseek-tui"
version = "0.3.0" version = "0.3.1"
edition = "2024" edition = "2024"
description = "Unofficial DeepSeek CLI - Just run 'deepseek' to start chatting" description = "Unofficial DeepSeek CLI - Just run 'deepseek' to start chatting"
license = "MIT" license = "MIT"
+16 -3
View File
@@ -39,6 +39,9 @@ cargo install deepseek-tui --locked
# Set your API key # Set your API key
export DEEPSEEK_API_KEY="YOUR_DEEPSEEK_API_KEY" export DEEPSEEK_API_KEY="YOUR_DEEPSEEK_API_KEY"
# Bootstrap MCP + skills templates (recommended)
deepseek setup
# Start chatting # Start chatting
deepseek deepseek
``` ```
@@ -88,6 +91,9 @@ Useful environment variables:
- `DEEPSEEK_CONFIG_PATH` (override config path) - `DEEPSEEK_CONFIG_PATH` (override config path)
- `DEEPSEEK_MCP_CONFIG`, `DEEPSEEK_SKILLS_DIR`, `DEEPSEEK_NOTES_PATH`, `DEEPSEEK_MEMORY_PATH`, `DEEPSEEK_ALLOW_SHELL`, `DEEPSEEK_MAX_SUBAGENTS` - `DEEPSEEK_MCP_CONFIG`, `DEEPSEEK_SKILLS_DIR`, `DEEPSEEK_NOTES_PATH`, `DEEPSEEK_MEMORY_PATH`, `DEEPSEEK_ALLOW_SHELL`, `DEEPSEEK_MAX_SUBAGENTS`
To bootstrap MCP and skills at their resolved locations, run `deepseek setup`. To
only create an MCP template, run `deepseek mcp init`.
See `config.example.toml` and `docs/CONFIGURATION.md` for a full reference. See `config.example.toml` and `docs/CONFIGURATION.md` for a full reference.
## 🎮 Modes ## 🎮 Modes
@@ -141,7 +147,7 @@ DeepSeek CLI exposes a comprehensive set of tools to the model across 5 categori
- **Workspace boundary**: File tools are restricted to `--workspace` unless you enable `/trust` (YOLO enables trust automatically). - **Workspace boundary**: File tools are restricted to `--workspace` unless you enable `/trust` (YOLO enables trust automatically).
- **Approvals**: The TUI requests approval depending on mode and tool category (file writes, shell). - **Approvals**: The TUI requests approval depending on mode and tool category (file writes, shell).
- **Web search**: `web_search` uses DuckDuckGo HTML results and is autoapproved. - **Web search**: `web_search` uses DuckDuckGo HTML results and is autoapproved.
- **Skills**: Reusable workflows stored as `SKILL.md` directories (default: `~/.deepseek/skills`). Use `/skills` and `/skill <name>`. - **Skills**: Reusable workflows stored as `SKILL.md` directories (default: `~/.deepseek/skills`, or `./skills` per workspace). Use `/skills` and `/skill <name>`. Bootstrap with `deepseek setup --skills` (add `--local` for `./skills`).
- **MCP**: Load external tool servers via `~/.deepseek/mcp.json` (supports `servers` and `mcpServers`). MCP tools currently execute without TUI approval prompts, so only enable servers you trust. See `docs/MCP.md`. - **MCP**: Load external tool servers via `~/.deepseek/mcp.json` (supports `servers` and `mcpServers`). MCP tools currently execute without TUI approval prompts, so only enable servers you trust. See `docs/MCP.md`.
## 🧠 RLM (Reasoning & Largescale Memory) ## 🧠 RLM (Reasoning & Largescale Memory)
@@ -240,11 +246,18 @@ Set `DEEPSEEK_BASE_URL` to `https://api.deepseeki.com` (China).
### Session issues ### Session issues
Run `deepseek sessions` and try `deepseek --resume latest`. Run `deepseek sessions` and try `deepseek --resume latest`.
### Skills missing
Run `deepseek setup --skills` to create a global skills directory, or add `--local`
to create `./skills` for the current workspace. Then run `deepseek doctor` to see
which skills directory is selected.
### MCP tools missing ### MCP tools missing
Validate `~/.deepseek/mcp.json` (or `DEEPSEEK_MCP_CONFIG`) and restart. Run `deepseek mcp init` (or `deepseek setup --mcp`), then restart. `deepseek doctor`
now checks the MCP path resolved from your config/env overrides.
### Sandbox errors (macOS) ### Sandbox errors (macOS)
Ensure `/usr/bin/sandbox-exec` exists (comes with macOS). For other platforms, sandboxing is limited. Run `deepseek doctor` to confirm sandbox availability. On macOS, ensure
`/usr/bin/sandbox-exec` exists. For other platforms, sandboxing is limited.
## 📖 Documentation ## 📖 Documentation
+9 -1
View File
@@ -15,6 +15,9 @@ Overrides:
If both are set, `--config` wins. Environment variable overrides are applied after the file is loaded. If both are set, `--config` wins. Environment variable overrides are applied after the file is loaded.
To bootstrap MCP and skills directories at their resolved paths, run `deepseek setup`.
To only scaffold MCP, run `deepseek mcp init`.
## Profiles ## Profiles
You can define multiple profiles in the same file: You can define multiple profiles in the same file:
@@ -124,4 +127,9 @@ Use `deepseek features list` to inspect known flags and their effective state.
## Notes On `deepseek doctor` ## Notes On `deepseek doctor`
`deepseek doctor` checks default locations under `~/.deepseek/` (including `config.toml` and `mcp.json`). If you override paths via `--config` or `DEEPSEEK_MCP_CONFIG`, the doctor output may not reflect those overrides. `deepseek doctor` now follows the same config resolution rules as the rest of the CLI.
That means `--config` / `DEEPSEEK_CONFIG_PATH` are respected, and MCP/skills checks
use the resolved `mcp_config_path` / `skills_dir` (including env overrides).
To bootstrap missing MCP/skills paths, run `deepseek setup --all`. You can also
run `deepseek setup --skills --local` to create a workspace-local `./skills` dir.
+14 -3
View File
@@ -2,6 +2,16 @@
DeepSeek CLI can load additional tools via MCP (Model Context Protocol). MCP servers are local processes that the CLI starts and communicates with over stdio. DeepSeek CLI can load additional tools via MCP (Model Context Protocol). MCP servers are local processes that the CLI starts and communicates with over stdio.
## Bootstrap MCP Config
Create a starter MCP config at your resolved MCP path:
```bash
deepseek mcp init
```
`deepseek setup --mcp` performs the same MCP bootstrap alongside skills setup.
## Config File Location ## Config File Location
Default path: Default path:
@@ -13,6 +23,8 @@ Overrides:
- Config: `mcp_config_path = "/path/to/mcp.json"` - Config: `mcp_config_path = "/path/to/mcp.json"`
- Env: `DEEPSEEK_MCP_CONFIG=/path/to/mcp.json` - Env: `DEEPSEEK_MCP_CONFIG=/path/to/mcp.json`
`deepseek mcp init` (and `deepseek setup --mcp`) writes to this resolved path.
After editing the file, restart the TUI. After editing the file, restart the TUI.
## Tool Naming ## Tool Naming
@@ -61,7 +73,6 @@ MCP tools currently execute without TUI approval prompts. Only configure MCP ser
## Troubleshooting ## Troubleshooting
- Run `deepseek doctor` to confirm whether the default `~/.deepseek/mcp.json` exists. - Run `deepseek doctor` to confirm the MCP config path it resolved and whether it exists.
- If you override `mcp_config_path` / `DEEPSEEK_MCP_CONFIG`, note that `deepseek doctor` still checks `~/.deepseek/mcp.json`. - If the MCP config is missing, run `deepseek mcp init --force` to regenerate it.
- If tools dont appear, verify the server command works from your shell and that the server supports MCP `tools/list`. - If tools dont appear, verify the server command works from your shell and that the server supports MCP `tools/list`.
+344 -64
View File
@@ -61,8 +61,9 @@ mod working_set;
use crate::config::{Config, MAX_SUBAGENTS}; use crate::config::{Config, MAX_SUBAGENTS};
use crate::eval::{EvalHarness, EvalHarnessConfig, ScenarioStepKind}; use crate::eval::{EvalHarness, EvalHarnessConfig, ScenarioStepKind};
use crate::features::Feature;
use crate::llm_client::LlmClient; use crate::llm_client::LlmClient;
use crate::mcp::{McpConfig, McpPool}; use crate::mcp::{McpConfig, McpPool, McpServerConfig};
use crate::models::{ContentBlock, Message, MessageRequest, SystemPrompt}; use crate::models::{ContentBlock, Message, MessageRequest, SystemPrompt};
use crate::session_manager::{SessionManager, create_saved_session}; use crate::session_manager::{SessionManager, create_saved_session};
use crate::tui::history::{summarize_tool_args, summarize_tool_output}; use crate::tui::history::{summarize_tool_args, summarize_tool_output};
@@ -133,6 +134,8 @@ struct Cli {
enum Commands { enum Commands {
/// Run system diagnostics and check configuration /// Run system diagnostics and check configuration
Doctor, Doctor,
/// Bootstrap MCP config and/or skills directories
Setup(SetupArgs),
/// Generate shell completions /// Generate shell completions
Completions { Completions {
/// Shell to generate completions for /// Shell to generate completions for
@@ -214,6 +217,25 @@ struct ExecArgs {
auto: bool, auto: bool,
} }
#[derive(Args, Debug, Clone, Default)]
struct SetupArgs {
/// Initialize MCP configuration at the configured path
#[arg(long, default_value_t = false)]
mcp: bool,
/// Initialize skills directory and an example skill
#[arg(long, default_value_t = false)]
skills: bool,
/// Initialize both MCP config and skills (default when no flags provided)
#[arg(long, default_value_t = false)]
all: bool,
/// Create a local workspace skills directory (./skills)
#[arg(long, default_value_t = false)]
local: bool,
/// Overwrite existing template files
#[arg(long, default_value_t = false)]
force: bool,
}
#[derive(Args, Debug, Clone)] #[derive(Args, Debug, Clone)]
struct EvalArgs { struct EvalArgs {
/// Intentionally fail a specific step (list, read, search, edit, patch, shell) /// Intentionally fail a specific step (list, read, search, edit, patch, shell)
@@ -293,6 +315,12 @@ struct ServeArgs {
enum McpCommand { enum McpCommand {
/// List configured MCP servers /// List configured MCP servers
List, List,
/// Create a template MCP config at the configured path
Init {
/// Overwrite an existing MCP config file
#[arg(long, default_value_t = false)]
force: bool,
},
/// Connect to MCP servers and report status /// Connect to MCP servers and report status
Connect { Connect {
/// Optional server name to connect to /// Optional server name to connect to
@@ -378,9 +406,16 @@ async fn main() -> Result<()> {
if let Some(command) = cli.command.clone() { if let Some(command) = cli.command.clone() {
return match command { return match command {
Commands::Doctor => { Commands::Doctor => {
run_doctor().await; let config = load_config_from_cli(&cli)?;
let workspace = resolve_workspace(&cli);
run_doctor(&config, &workspace, cli.config.as_deref()).await;
Ok(()) Ok(())
} }
Commands::Setup(args) => {
let config = load_config_from_cli(&cli)?;
let workspace = resolve_workspace(&cli);
run_setup(&config, &workspace, args)
}
Commands::Completions { shell } => { Commands::Completions { shell } => {
generate_completions(shell); generate_completions(shell);
Ok(()) Ok(())
@@ -562,8 +597,175 @@ fn run_eval(args: EvalArgs) -> Result<()> {
} }
} }
#[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: "node".to_string(),
args: vec!["./path/to/your-mcp-server.js".to_string()],
env: std::collections::HashMap::new(),
connect_timeout: None,
execute_timeout: None,
read_timeout: None,
disabled: true,
},
);
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 run_setup(config: &Config, workspace: &Path, args: SetupArgs) -> 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 mut run_mcp = args.mcp || args.all;
let mut run_skills = args.skills || args.all;
if !run_mcp && !run_skills {
run_mcp = true;
run_skills = true;
}
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`.");
}
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(())
}
/// Run system diagnostics /// Run system diagnostics
async fn run_doctor() { async fn run_doctor(config: &Config, workspace: &Path, config_path_override: Option<&Path>) {
use crate::palette; use crate::palette;
use colored::Colorize; use colored::Colorize;
@@ -587,24 +789,29 @@ async fn run_doctor() {
println!(" rust: {}", rustc_version()); println!(" rust: {}", rustc_version());
println!(); println!();
// Check configuration // Configuration summary
println!("{}", "Configuration:".bold()); println!("{}", "Configuration:".bold());
let config_dir = let default_config_dir =
dirs::home_dir().map_or_else(|| PathBuf::from(".deepseek"), |h| h.join(".deepseek")); 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 config_file = config_dir.join("config.toml"); if config_path.exists() {
if config_file.exists() {
println!( println!(
" {} config.toml found at {}", " {} config.toml found at {}",
"".truecolor(aqua_r, aqua_g, aqua_b), "".truecolor(aqua_r, aqua_g, aqua_b),
config_file.display() config_path.display()
); );
} else { } else {
println!( println!(
" {} config.toml not found (will use defaults)", " {} config.toml not found at {} (using defaults/env)",
"!".truecolor(sky_r, sky_g, sky_b) "!".truecolor(sky_r, sky_g, sky_b),
config_path.display()
); );
} }
println!(" workspace: {}", workspace.display());
// Check API keys // Check API keys
println!(); println!();
@@ -615,25 +822,19 @@ async fn run_doctor() {
"".truecolor(aqua_r, aqua_g, aqua_b) "".truecolor(aqua_r, aqua_g, aqua_b)
); );
true true
} else if config.deepseek_api_key().is_ok() {
println!(
" {} DeepSeek API key found in effective config",
"".truecolor(aqua_r, aqua_g, aqua_b)
);
true
} else { } else {
let key_in_config = Config::load(None, None) println!(
.ok() " {} DeepSeek API key not configured",
.and_then(|c| c.deepseek_api_key().ok()) "".truecolor(red_r, red_g, red_b)
.is_some(); );
if key_in_config { println!(" Run 'deepseek' to configure interactively, or set DEEPSEEK_API_KEY");
println!( false
" {} DeepSeek API key found in config",
"".truecolor(aqua_r, aqua_g, aqua_b)
);
true
} else {
println!(
" {} DeepSeek API key not configured",
"".truecolor(red_r, red_g, red_b)
);
println!(" Run 'deepseek' to configure interactively, or set DEEPSEEK_API_KEY");
false
}
}; };
// API connectivity test // API connectivity test
@@ -641,7 +842,6 @@ async fn run_doctor() {
println!("{}", "API Connectivity:".bold()); println!("{}", "API Connectivity:".bold());
if has_api_key { if has_api_key {
print!(" {} Testing connection to DeepSeek API...", "·".dimmed()); print!(" {} Testing connection to DeepSeek API...", "·".dimmed());
// Flush to show progress immediately
use std::io::Write; use std::io::Write;
std::io::stdout().flush().ok(); std::io::stdout().flush().ok();
@@ -659,7 +859,6 @@ async fn run_doctor() {
"\r {} API connection failed", "\r {} API connection failed",
"".truecolor(red_r, red_g, red_b) "".truecolor(red_r, red_g, red_b)
); );
// Provide helpful diagnostics based on error type
if error_msg.contains("401") || error_msg.contains("Unauthorized") { if error_msg.contains("401") || error_msg.contains("Unauthorized") {
println!(" Invalid API key. Check your DEEPSEEK_API_KEY or config.toml"); println!(" Invalid API key. Check your DEEPSEEK_API_KEY or config.toml");
} else if error_msg.contains("403") || error_msg.contains("Forbidden") { } else if error_msg.contains("403") || error_msg.contains("Forbidden") {
@@ -681,68 +880,124 @@ async fn run_doctor() {
println!(" {} Skipped (no API key configured)", "·".dimmed()); println!(" {} Skipped (no API key configured)", "·".dimmed());
} }
// Check MCP configuration // MCP configuration
println!(); println!();
println!("{}", "MCP Servers:".bold()); println!("{}", "MCP Servers:".bold());
let mcp_config = config_dir.join("mcp.json"); let features = config.features();
if mcp_config.exists() { if features.enabled(Feature::Mcp) {
println!(" {} mcp.json found", "".truecolor(aqua_r, aqua_g, aqua_b)); println!(" {} MCP feature flag enabled", "".truecolor(aqua_r, aqua_g, aqua_b));
if let Ok(content) = std::fs::read_to_string(&mcp_config) } else {
&& let Ok(config) = serde_json::from_str::<crate::mcp::McpConfig>(&content) println!(" {} MCP feature flag disabled", "!".truecolor(sky_r, sky_g, sky_b));
{ }
if config.servers.is_empty() {
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()); println!(" {} 0 server(s) configured", "·".dimmed());
} else { }
Ok(cfg) => {
println!( println!(
" {} {} server(s) configured", " {} {} server(s) configured",
"·".dimmed(), "·".dimmed(),
config.servers.len() cfg.servers.len()
); );
for name in config.servers.keys() { for name in cfg.servers.keys() {
println!(" - {name}"); println!(" - {name}");
} }
} }
Err(err) => {
println!(
" {} MCP config parse error: {}",
"".truecolor(red_r, red_g, red_b),
err
);
}
} }
} else { } else {
println!(" {} mcp.json not found (no MCP servers)", "·".dimmed()); println!(
" {} MCP config not found at {}",
"·".dimmed(),
mcp_config_path.display()
);
println!(" Run `deepseek mcp init` or `deepseek setup --mcp`.");
} }
// Check skills directory // Skills configuration
println!(); println!();
println!("{}", "Skills:".bold()); println!("{}", "Skills:".bold());
let skills_dir = config_dir.join("skills"); let global_skills_dir = config.skills_dir();
if skills_dir.exists() { let local_skills_dir = workspace.join("skills");
let skill_count = std::fs::read_dir(skills_dir) let selected_skills_dir = 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()) .map(|entries| entries.filter_map(std::result::Result::ok).count())
.unwrap_or(0); .unwrap_or(0)
};
if local_skills_dir.exists() {
println!( println!(
" {} skills directory found ({} items)", " {} local skills dir found at {} ({} items)",
"".truecolor(aqua_r, aqua_g, aqua_b), "".truecolor(aqua_r, aqua_g, aqua_b),
skill_count local_skills_dir.display(),
describe_dir(&local_skills_dir)
); );
} else { } else {
println!(" {} skills directory not found", "·".dimmed()); println!(
" {} local skills dir not found at {}",
"·".dimmed(),
local_skills_dir.display()
);
} }
// Platform-specific checks 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 !local_skills_dir.exists() && !global_skills_dir.exists() {
println!(" Run `deepseek setup --skills` (or add --local for ./skills).");
}
// Platform and sandbox checks
println!(); println!();
println!("{}", "Platform:".bold()); println!("{}", "Platform:".bold());
println!(" OS: {}", std::env::consts::OS); println!(" OS: {}", std::env::consts::OS);
println!(" Arch: {}", std::env::consts::ARCH); println!(" Arch: {}", std::env::consts::ARCH);
#[cfg(target_os = "macos")] let sandbox = crate::sandbox::get_platform_sandbox();
{ if let Some(kind) = sandbox {
if std::path::Path::new("/usr/bin/sandbox-exec").exists() { println!(
println!( " {} sandbox available: {}",
" {} macOS sandbox available", "".truecolor(aqua_r, aqua_g, aqua_b),
"".truecolor(aqua_r, aqua_g, aqua_b) kind
); );
} else { } else {
println!( println!(
" {} macOS sandbox not available", " {} sandbox not available (commands run best-effort)",
"!".truecolor(sky_r, sky_g, sky_b) "!".truecolor(sky_r, sky_g, sky_b)
); );
}
} }
println!(); println!();
@@ -946,6 +1201,12 @@ fn init_project() -> Result<()> {
Ok(()) 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> { fn load_config_from_cli(cli: &Cli) -> Result<Config> {
let profile = cli let profile = cli
.profile .profile
@@ -1173,6 +1434,25 @@ fn read_patch_from_stdin() -> Result<String> {
async fn run_mcp_command(config: &Config, command: McpCommand) -> Result<()> { async fn run_mcp_command(config: &Config, command: McpCommand) -> Result<()> {
let config_path = config.mcp_config_path(); let config_path = config.mcp_config_path();
match command { 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 => { McpCommand::List => {
let cfg = load_mcp_config(&config_path)?; let cfg = load_mcp_config(&config_path)?;
if cfg.servers.is_empty() { if cfg.servers.is_empty() {