#![warn(clippy::all, clippy::pedantic)]
#![allow(
clippy::assigning_clones,
clippy::bool_to_int_with_if,
clippy::case_sensitive_file_extension_comparisons,
clippy::cast_possible_wrap,
clippy::doc_markdown,
clippy::field_reassign_with_default,
clippy::float_cmp,
clippy::implicit_clone,
clippy::items_after_statements,
clippy::map_unwrap_or,
clippy::manual_let_else,
clippy::missing_errors_doc,
clippy::missing_panics_doc,
clippy::large_futures,
clippy::module_name_repetitions,
clippy::needless_pass_by_value,
clippy::needless_raw_string_hashes,
clippy::redundant_closure_for_method_calls,
clippy::similar_names,
clippy::single_match_else,
clippy::struct_field_names,
clippy::too_many_lines,
clippy::uninlined_format_args,
clippy::unused_self,
clippy::cast_precision_loss,
clippy::unnecessary_cast,
clippy::unnecessary_lazy_evaluations,
clippy::unnecessary_literal_bound,
clippy::unnecessary_map_or,
clippy::unnecessary_wraps,
dead_code
)]
use anyhow::{Context, Result, bail};
use clap::Parser;
use tracing::info;
use tracing_subscriber::{EnvFilter, fmt};
use vw_agent::channels::ChannelCommands;
use vw_agent::cron;
use vw_agent::integrations;
use vw_agent::provider::provider;
use vw_agent::{
channels, config, daemon, doctor, gateway, memory, observability, security, service, skills,
};
use vw_shared::task::{self, SubTask, Task, TaskExecutorBackend, TaskStatus};
use config::Config;
use config::schema::ChannelsConfigExt;
use config::schema::ConfigExt;
#[path = "cli.rs"]
mod cli;
#[cfg(test)]
#[path = "cli_tests.rs"]
mod cli_tests;
mod handlers;
pub(crate) mod session {
pub(crate) use vw_shared::session::ui_types;
}
pub(crate) mod app {
pub(crate) mod agent {
pub(crate) use vw_agent::{
approval, channels, config, id, memory, observability, project, providers, runtime,
security, shell, skills, tools,
};
pub(crate) mod session {
pub(crate) use vw_agent::session::{processor, session, title};
}
#[allow(clippy::module_inception)]
pub(crate) mod agent {
pub(crate) mod loop_ {
pub(crate) use vw_agent::agent::loop_::{context, core, instructions, progress};
pub(crate) mod cli {
pub(crate) use crate::cli::legacy_runtime::{
logo_text_lines, render_execution_indicator,
};
pub(crate) use crate::cli::legacy_runtime::{theme, transcript, tui_utils};
}
}
}
}
}
use cli::{Cli, Commands, ConfigCommands, DoctorCommands, TaskCommands};
fn parse_temperature(s: &str) -> std::result::Result<f64, String> {
let t: f64 = s.parse().map_err(|e| format!("{e}"))?;
if !(0.0..=2.0).contains(&t) {
return Err("temperature must be between 0.0 and 2.0".to_string());
}
Ok(t)
}
fn resolve_project_dir(project_dir: &str) -> Result<String> {
let trimmed = project_dir.trim();
if trimmed.is_empty() {
bail!("--project-dir cannot be empty");
}
let path = std::fs::canonicalize(trimmed)
.with_context(|| format!("Failed to resolve project directory: {trimmed}"))?;
if !path.is_dir() {
bail!("project directory is not a folder: {}", path.display());
}
Ok(path.to_string_lossy().to_string())
}
fn first_non_empty_line(content: &str) -> String {
content.lines().map(str::trim).find(|line| !line.is_empty()).unwrap_or_default().to_string()
}
fn print_task_json(task: &Task) -> Result<()> {
println!("{}", serde_json::to_string_pretty(task)?);
Ok(())
}
fn print_tasks_json(tasks: &[Task]) -> Result<()> {
println!("{}", serde_json::to_string_pretty(tasks)?);
Ok(())
}
#[allow(clippy::too_many_lines)]
pub async fn run() -> Result<()> {
if let Err(e) = rustls::crypto::ring::default_provider().install_default() {
eprintln!("Warning: Failed to install default crypto provider: {e:?}");
}
let cli = Cli::parse();
if let Some(config_dir) = &cli.config_dir {
if config_dir.trim().is_empty() {
bail!("--config-dir cannot be empty");
}
unsafe {
std::env::set_var("VIBEWINDOW_CONFIG_DIR", config_dir);
}
}
if let Commands::Completions { shell } = &cli.command {
let mut stdout = std::io::stdout().lock();
cli::write_shell_completion(*shell, &mut stdout)?;
return Ok(());
}
let interactive_agent_mode = matches!(&cli.command, Commands::Agent { message: None, .. });
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
if interactive_agent_mode {
let subscriber = fmt::Subscriber::builder()
.with_timer(tracing_subscriber::fmt::time::ChronoLocal::rfc_3339())
.with_env_filter(env_filter)
.with_writer(std::io::sink)
.finish();
tracing::subscriber::set_global_default(subscriber)
.expect("setting default subscriber failed");
} else {
let subscriber = fmt::Subscriber::builder()
.with_timer(tracing_subscriber::fmt::time::ChronoLocal::rfc_3339())
.with_env_filter(env_filter)
.finish();
tracing::subscriber::set_global_default(subscriber)
.expect("setting default subscriber failed");
}
let mut config = Box::pin(Config::load_or_init()).await?;
config.apply_env_overrides();
observability::runtime_trace::init_from_config(&config.observability, &config.workspace_dir);
if config.security.otp.enabled {
let config_dir =
config.config_path.parent().context("Config path must have a parent directory")?;
let store = security::SecretStore::new(config_dir, config.secrets.encrypt);
let (_validator, enrollment_uri) =
security::OtpValidator::from_config(&config.security.otp, config_dir, &store)?;
if let Some(uri) = enrollment_uri {
println!("Initialized OTP secret for VibeWindow.");
println!("Enrollment URI: {uri}");
}
}
match cli.command {
Commands::Completions { .. } => unreachable!(),
Commands::Agent {
message,
tui_mode,
provider,
model,
temperature,
peripheral,
autonomy_level,
max_actions_per_hour,
max_tool_iterations,
max_history_messages,
compact_context,
memory_backend,
} => {
if let Some(level) = autonomy_level {
config.autonomy.level = level;
}
if let Some(n) = max_actions_per_hour {
config.autonomy.max_actions_per_hour = n;
}
if let Some(n) = max_tool_iterations {
config.agent.max_tool_iterations = n;
}
if let Some(n) = max_history_messages {
config.agent.max_history_messages = n;
}
if compact_context {
config.agent.compact_context = true;
}
if let Some(ref backend) = memory_backend {
config.memory.backend = backend.clone();
}
Box::pin(crate::cli::run(
config,
message,
provider,
model,
temperature,
peripheral,
interactive_agent_mode,
tui_mode,
))
.await
.map(|_| ())
}
Commands::Gateway { port, host, new_pairing } => {
if new_pairing {
let mut persisted_config = Box::pin(Config::load_or_init()).await?;
persisted_config.gateway.paired_tokens.clear();
persisted_config.save().await?;
config.gateway.paired_tokens.clear();
info!("🔐 Cleared paired tokens — a fresh pairing code will be generated");
}
let port = port.unwrap_or(config.gateway.port);
let host = host.unwrap_or_else(|| config.gateway.host.clone());
if port == 0 {
info!("🚀 Starting VibeWindow Gateway on {host} (random port)");
} else {
info!("🚀 Starting VibeWindow Gateway on {host}:{port}");
}
gateway::run_gateway(&host, port, config).await
}
Commands::Daemon { port, host } => {
let port = port.unwrap_or(config.gateway.port);
let host = host.unwrap_or_else(|| config.gateway.host.clone());
if port == 0 {
info!("💡 Starting VibeWindow Daemon on {host} (random port)");
} else {
info!("💡 Starting VibeWindow Daemon on {host}:{port}");
}
daemon::run(config, host, port).await
}
Commands::Status => {
println!("🦀 VibeWindow Status");
println!();
println!("Version: {}", env!("CARGO_PKG_VERSION"));
println!("Workspace: {}", config.workspace_dir.display());
println!("Config: {}", config.config_path.display());
println!();
println!(
"🤖 Provider: {}",
config.default_provider.as_deref().unwrap_or("openrouter")
);
println!(
" Model: {}",
config.default_model.as_deref().unwrap_or("(default)")
);
println!("📊 Observability: {}", config.observability.backend);
println!(
"🧾 Trace storage: {} ({})",
config.observability.runtime_trace_mode, config.observability.runtime_trace_path
);
println!("🛡️ Autonomy: {:?}", config.autonomy.level);
println!("⚙️ Runtime: {}", config.runtime.kind);
let effective_memory_backend = memory::effective_memory_backend_name(
&config.memory.backend,
Some(&config.storage.provider.config),
);
println!(
"💓 Heartbeat: {}",
if config.heartbeat.enabled {
format!("every {}min", config.heartbeat.interval_minutes)
} else {
"disabled".into()
}
);
println!(
"💡 Memory: {} (auto-save: {})",
effective_memory_backend,
if config.memory.auto_save { "on" } else { "off" }
);
println!();
println!("Security:");
println!(" Workspace only: {}", config.autonomy.workspace_only);
println!(
" Allowed roots: {}",
if config.autonomy.allowed_roots.is_empty() {
"(none)".to_string()
} else {
config.autonomy.allowed_roots.join(", ")
}
);
println!(" Allowed commands: {}", config.autonomy.allowed_commands.join(", "));
println!(" Max actions/hour: {}", config.autonomy.max_actions_per_hour);
println!(
" Max cost/day: ${:.2}",
f64::from(config.autonomy.max_cost_per_day_cents) / 100.0
);
println!(" OTP enabled: {}", config.security.otp.enabled);
println!(" E-stop enabled: {}", config.security.estop.enabled);
println!();
println!("Channels:");
println!(" CLI: ✅ always");
for (channel, configured) in config.channels_config.channels() {
println!(
" {:9} {}",
channel.name(),
if configured { "✅ configured" } else { "❌ not configured" }
);
}
Ok(())
}
Commands::Estop { estop_command, level, domains, tools } => {
handlers::estop::handle_estop_command(&config, estop_command, level, domains, tools)
}
Commands::Security { security_command } => {
handlers::security::handle_security_command(&config, security_command).await
}
Commands::Cron { cron_command } => cron::handle_command(cron_command, &config),
Commands::Providers => {
let provider_map = provider::list().await;
let mut providers = provider_map.values().collect::<Vec<_>>();
providers.sort_by(|a, b| a.id.cmp(&b.id));
let current = config
.default_provider
.as_deref()
.unwrap_or("openrouter")
.trim()
.to_ascii_lowercase();
println!("Supported providers ({} total):\n", providers.len());
println!(" ID (use in config) DESCRIPTION");
println!(" ─────────────────── ───────────");
for p in &providers {
let is_active = p.id.eq_ignore_ascii_case(¤t);
let marker = if is_active { " (active)" } else { "" };
println!(" {:<19} {}{}", p.id, p.name, marker);
}
println!("\n custom:<URL> Any OpenAI-compatible endpoint");
println!(" anthropic-custom:<URL> Any Anthropic-compatible endpoint");
Ok(())
}
Commands::Service { service_command, service_init } => {
let init_system = service_init.parse()?;
service::handle_command(&service_command, &config, init_system)
}
Commands::Doctor { doctor_command } => match doctor_command {
Some(DoctorCommands::Models { provider, use_cache }) => {
doctor::run_models(&config, provider.as_deref(), use_cache).await
}
Some(DoctorCommands::Traces { id, event, contains, limit }) => doctor::run_traces(
&config,
id.as_deref(),
event.as_deref(),
contains.as_deref(),
limit,
),
None => doctor::run(&config),
},
Commands::Channel { channel_command } => match channel_command {
ChannelCommands::Start => Box::pin(channels::start_channels(config)).await,
ChannelCommands::Doctor => channels::doctor_channels(config).await,
other => channels::handle_command(other, &config).await,
},
Commands::Integrations { integration_command } => {
integrations::handle_command(integration_command, &config)
}
Commands::Skills { skill_command } => skills::handle_command(skill_command, &config),
Commands::Task { project_dir, task_command } => {
let project_path = resolve_project_dir(&project_dir)?;
match task_command {
TaskCommands::Create {
priority,
prompt,
description,
assignee,
model,
executor,
subtasks,
} => {
let prompt = prompt.map(|value| value.trim().to_string());
let description = description.map(|value| value.trim().to_string());
let cleaned_subtasks = subtasks
.into_iter()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect::<Vec<_>>();
let mut task_seed = first_non_empty_line(prompt.as_deref().unwrap_or_default());
if task_seed.is_empty() {
task_seed =
first_non_empty_line(description.as_deref().unwrap_or_default());
}
if task_seed.is_empty() {
task_seed = cleaned_subtasks.first().cloned().unwrap_or_default();
}
if task_seed.is_empty() {
bail!(
"task content is empty; provide at least one of --prompt, --description, or --subtask"
);
}
let mut task = Task::new(priority);
if let Some(description) = description {
task.description = description;
}
if let Some(assignee) = assignee {
let assignee = assignee.trim();
if !assignee.is_empty() {
task.assignee = assignee.to_string();
}
}
if let Some(model) = model {
let model = model.trim();
if !model.is_empty() {
task.model = model.to_string();
}
}
if let Some(executor) = executor {
let parsed = TaskExecutorBackend::from_id(executor.trim())
.with_context(|| {
format!(
"Invalid --executor value: {} (supported: internal, opencode, claude, codex)",
executor
)
})?;
task.executor = parsed;
}
task.subtasks = cleaned_subtasks.into_iter().map(SubTask::new).collect();
match prompt {
Some(prompt) if !prompt.is_empty() => {
task.prompt = prompt;
}
_ => {
task.prompt = first_non_empty_line(&task.description);
}
}
if task.prompt.is_empty()
&& let Some(subtask) = task.subtasks.first()
{
task.prompt = subtask.content.clone();
}
let created = task::create_task(&project_path, task).with_context(|| {
format!("Failed to create task in {}", project_path.as_str())
})?;
print_task_json(&created)
}
TaskCommands::Read { id, status, include_archived, include_deleted, limit } => {
if let Some(task_id) = id {
let task =
task::load_task(&project_path, task_id.trim()).with_context(|| {
format!(
"Task not found in {} with id {}",
project_path.as_str(),
task_id
)
})?;
return print_task_json(&task);
}
let status_filter = status
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(|value| {
TaskStatus::parse_key(value)
.with_context(|| format!("Invalid --status value: {value}"))
})
.transpose()?;
let mut tasks = task::load_all_tasks(&project_path);
tasks.retain(|task| {
let status_ok = status_filter.is_none_or(|s| task.status == s);
let archived_ok = include_archived || !task.archived;
let deleted_ok = include_deleted || !task.deleted;
status_ok && archived_ok && deleted_ok
});
tasks.sort_by(|a, b| {
b.created_at_ms
.cmp(&a.created_at_ms)
.then_with(|| a.priority.cmp(&b.priority))
});
if tasks.len() > limit {
tasks.truncate(limit);
}
print_tasks_json(&tasks)
}
}
}
Commands::Memory { memory_command } => {
memory::cli::handle_command(memory_command, &config).await
}
Commands::Config { config_command } => match config_command {
ConfigCommands::Schema => {
let schema = schemars::schema_for!(config::Config);
println!(
"{}",
serde_json::to_string_pretty(&schema).expect("failed to serialize JSON Schema")
);
Ok(())
}
},
}
}
#[cfg(test)]
#[path = "tests.rs"]
mod tests;