use uuid::Uuid;
use crate::brain::agent::AgentService;
use crate::config::Config;
use crate::db::repository::SessionListOptions;
use crate::services::SessionService;
pub async fn sync_provider_for_session(
agent: &AgentService,
session_id: Uuid,
session_provider: Option<&str>,
session_model: Option<&str>,
) {
let config = match Config::load() {
Ok(c) => c,
Err(e) => {
tracing::warn!(
"sync_provider_for_session[{}]: Config::load failed: {} — skipping sync",
session_id,
e
);
return;
}
};
if let Some(sess_prov) = session_provider {
let agent_provider = agent.provider_name_for_session(session_id);
let agent_model = agent.provider_model_for_session(session_id);
let sess_prov_norm = normalize_provider_name(sess_prov);
let agent_prov_norm = normalize_provider_name(&agent_provider);
let same_provider = provider_names_match(&sess_prov_norm, &agent_prov_norm);
let same_model = session_model.is_none_or(|m| m == agent_model);
if same_provider && same_model {
tracing::debug!(
"sync_provider_for_session[{}]: already on {}/{} — no swap needed",
session_id,
agent_provider,
agent_model
);
return;
}
tracing::info!(
"sync_provider_for_session[{}]: session wants {}/{}, agent currently on {}/{} — attempting restore",
session_id,
sess_prov,
session_model.unwrap_or("<default>"),
agent_provider,
agent_model,
);
let has_key = config
.providers
.custom
.as_ref()
.and_then(|c| c.get(sess_prov))
.and_then(|p| p.api_key.as_ref())
.is_some_and(|k| !k.is_empty() && k != "__EXISTING_KEY__");
tracing::info!(
"sync_provider_for_session[{}]: create_provider_by_name('{}') — config has api_key: {}",
session_id,
sess_prov,
has_key
);
match crate::brain::provider::factory::create_provider_by_name(&config, sess_prov).await {
Ok(new_provider) => {
tracing::info!(
"sync_provider_for_session[{}]: restored {}/{} (was {}/{})",
session_id,
sess_prov,
session_model.unwrap_or("<default>"),
agent_provider,
agent_model,
);
let model = session_model
.map(str::to_string)
.unwrap_or_else(|| new_provider.default_model().to_string());
agent.swap_provider_for_session(session_id, new_provider, model);
}
Err(e) => {
tracing::warn!(
"sync_provider_for_session[{}]: create_provider_by_name('{}') failed: {} — keeping current provider (session isolation)",
session_id,
sess_prov,
e
);
}
}
} else {
tracing::debug!(
"sync_provider_for_session[{}]: session has no stored provider — using global config",
session_id
);
let (cfg_provider, cfg_model) = config.providers.active_provider_and_model();
let agent_provider = agent.provider_name_for_session(session_id);
let agent_model = agent.provider_model_for_session(session_id);
let cfg_provider_norm = normalize_provider_name(&cfg_provider);
let agent_provider_norm = normalize_provider_name(&agent_provider);
let same_provider = provider_names_match(&cfg_provider_norm, &agent_provider_norm);
if !same_provider || cfg_model != agent_model {
match crate::brain::provider::create_provider(&config).await {
Ok(new_provider) => {
tracing::info!(
"sync_provider_for_session[{}]: synced to config provider {} (was {})",
session_id,
cfg_provider,
agent_provider,
);
agent.swap_provider_for_session(session_id, new_provider, cfg_model.clone());
}
Err(e) => {
tracing::warn!(
"sync_provider_for_session[{}]: create_provider(active config) failed: {}",
session_id,
e
);
}
}
}
}
}
pub(crate) fn normalize_provider_name(name: &str) -> String {
crate::utils::providers::normalize_provider_name(name)
}
pub(crate) fn provider_names_match(config_provider: &str, runtime_provider: &str) -> bool {
config_provider == runtime_provider
|| config_provider
.strip_prefix("custom:")
.is_some_and(|name| name == runtime_provider)
}
pub enum ChannelCommand {
Help(String),
Usage(String),
Models(ProvidersResponse),
NewSession,
Sessions(SessionsResponse),
Stop,
Compact,
Doctor,
Evolve,
Rtk(String),
UserPrompt(String),
UserSystem(String),
UnknownCommand(String),
NotACommand,
}
pub struct ProvidersResponse {
pub current_provider: String,
pub current_model: String,
pub providers: Vec<(String, String, bool)>,
pub text: String,
}
pub struct SessionsResponse {
pub current_session_id: Uuid,
pub sessions: Vec<(Uuid, String)>,
pub text: String,
}
pub struct ModelsResponse {
pub provider_name: String,
pub current_model: String,
pub models: Vec<String>,
pub text: String,
pub agent_handled: bool,
}
pub async fn handle_command(
text: &str,
session_id: Uuid,
agent: &AgentService,
session_svc: &SessionService,
) -> ChannelCommand {
let trimmed = text.trim();
let result = match trimmed {
"/compact" => ChannelCommand::Compact,
"/doctor" => ChannelCommand::Doctor,
"/evolve" => ChannelCommand::Evolve,
"/help" => ChannelCommand::Help(format_help()),
"/models" => ChannelCommand::Models(format_providers(agent)),
"/new" => ChannelCommand::NewSession,
"/rtk" => ChannelCommand::Rtk(format_rtk().await),
"/sessions" => ChannelCommand::Sessions(format_sessions(session_id, session_svc).await),
"/stop" => ChannelCommand::Stop,
"/usage" => ChannelCommand::Usage(format_usage(session_id, agent, session_svc).await),
_ if trimmed.starts_with('/') && !crate::utils::string::looks_like_file_path(trimmed) => {
match_user_command(trimmed)
}
_ => ChannelCommand::NotACommand,
};
let response_text = match &result {
ChannelCommand::Help(body) | ChannelCommand::Usage(body) => Some(body.clone()),
ChannelCommand::Models(resp) => Some(resp.text.clone()),
ChannelCommand::Sessions(resp) => Some(resp.text.clone()),
ChannelCommand::NewSession => Some("New session started.".to_string()),
ChannelCommand::Stop => Some("Operation stopped.".to_string()),
ChannelCommand::UserSystem(body) => Some(body.clone()),
ChannelCommand::Doctor => Some("Running health check...".to_string()),
ChannelCommand::Evolve => Some("Checking for updates...".to_string()),
ChannelCommand::Rtk(body) => Some(body.clone()),
ChannelCommand::Compact
| ChannelCommand::UserPrompt(_)
| ChannelCommand::NotACommand
| ChannelCommand::UnknownCommand(_) => None,
};
if let Some(response) = response_text {
persist_command_to_history(agent, session_id, trimmed, &response).await;
}
result
}
async fn persist_command_to_history(
agent: &AgentService,
session_id: Uuid,
command: &str,
response: &str,
) {
let msg_svc = crate::services::MessageService::new(agent.context().clone());
if let Err(e) = msg_svc
.create_message(session_id, "user".to_string(), command.to_string())
.await
{
tracing::warn!("Failed to persist channel command to history: {}", e);
}
if let Err(e) = msg_svc
.create_message(session_id, "assistant".to_string(), response.to_string())
.await
{
tracing::warn!(
"Failed to persist channel command response to history: {}",
e
);
}
if let Some(tx) = agent.session_updated_tx() {
let _ = tx.send(crate::brain::agent::ChannelSessionEvent::Updated(
session_id,
));
}
}
fn match_user_command(text: &str) -> ChannelCommand {
let brain_path = crate::brain::BrainLoader::resolve_path();
let loader = crate::brain::CommandLoader::from_brain_path(&brain_path);
let commands = loader.load();
let skills = crate::brain::skills::load_all_skills();
match_user_command_inner(text, &commands, &skills)
}
pub(crate) fn match_user_command_inner(
text: &str,
commands: &[crate::brain::commands::UserCommand],
skills: &[crate::brain::skills::Skill],
) -> ChannelCommand {
let (cmd_name, args) = text
.split_once(' ')
.map(|(c, a)| (c, a.trim()))
.unwrap_or((text, ""));
if let Some(cmd) = commands.iter().find(|c| c.name == cmd_name) {
let prompt = if args.is_empty() {
cmd.prompt.clone()
} else {
format!("{} {}", cmd.prompt, args)
};
return match cmd.action.as_str() {
"system" => ChannelCommand::UserSystem(prompt),
_ => ChannelCommand::UserPrompt(prompt),
};
}
if let Some(skill) = skills.iter().find(|s| s.slash_name == cmd_name) {
let prompt = if args.is_empty() {
skill.body.clone()
} else {
format!("{}\n\n{}", skill.body, args)
};
return ChannelCommand::UserPrompt(prompt);
}
ChannelCommand::UnknownCommand(format!(
"⚡ Unknown command: {}. Type /help for available commands.",
cmd_name
))
}
pub(crate) fn format_help() -> String {
let mut lines = vec![
"📖 *Available Commands*".to_string(),
String::new(),
"`/compact` — Compact context (summarize & trim)".to_string(),
"`/evolve` — Download latest release & restart".to_string(),
"`/help` — Show this message".to_string(),
"`/models` — Switch AI model".to_string(),
"`/new` — Start a new session".to_string(),
"`/rtk` — Show RTK token savings statistics".to_string(),
"`/sessions` — Switch between sessions".to_string(),
"`/stop` — Abort current operation".to_string(),
"`/usage` — Session token & cost stats".to_string(),
];
let brain_path = crate::brain::BrainLoader::resolve_path();
let loader = crate::brain::CommandLoader::from_brain_path(&brain_path);
let mut user_cmds = loader.load();
if !user_cmds.is_empty() {
user_cmds.sort_by(|a, b| a.name.cmp(&b.name));
lines.push(String::new());
lines.push("📌 *Custom Commands*".to_string());
for cmd in &user_cmds {
lines.push(format!("`{}` — {}", cmd.name, cmd.description));
}
}
lines.push(String::new());
lines.push("🦀 Any other message is sent to OpenCrabs. 🦀".to_string());
lines.join("\n")
}
#[cfg(feature = "rtk")]
async fn format_rtk() -> String {
match tokio::process::Command::new("rtk")
.arg("gain")
.output()
.await
{
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
if output.status.success() {
format!("📊 *RTK Token Savings:*\n\n```\n{}\n```", stdout.trim())
} else {
format!("⚠️ RTK gain command failed:\n\n```\n{}\n```", stderr.trim())
}
}
Err(e) => {
format!("⚠️ Failed to run rtk gain: {}. Is RTK installed?", e)
}
}
}
#[cfg(not(feature = "rtk"))]
async fn format_rtk() -> String {
"⚠️ RTK feature is not enabled. Rebuild with --features rtk to enable token savings tracking."
.to_string()
}
async fn format_usage(
session_id: Uuid,
agent: &AgentService,
session_svc: &SessionService,
) -> String {
use crate::usage::data::{DashboardData, Period, fmt_cost, fmt_tokens};
let mut lines = vec!["📊 *Usage Dashboard*".to_string(), String::new()];
let current_model = agent.provider_model();
match session_svc.get_session(session_id).await {
Ok(Some(session)) => {
let name = session.title.as_deref().unwrap_or("Current Session");
let model = session
.model
.as_deref()
.filter(|m| !m.is_empty())
.unwrap_or(¤t_model);
let tokens = session.token_count;
let cost = if session.total_cost > 0.0 {
session.total_cost
} else if tokens > 0 {
estimate_cost(model, tokens as i64).unwrap_or(0.0)
} else {
0.0
};
lines.push(format!("*Current:* {}", name));
lines.push(format!(
" `{}` · {} tok · ${:.4}",
model,
format_number(tokens as i64),
cost
));
}
_ => {
lines.push("*Current:* (session not found)".to_string());
}
}
lines.push(String::new());
for period in [Period::Today, Period::AllTime] {
let pool = session_svc.pool();
let data = match DashboardData::fetch(&pool, period).await {
Ok(d) => d,
Err(e) => {
lines.push(format!("*{}:* (failed to load: {})", period.label(), e));
lines.push(String::new());
continue;
}
};
lines.push(format!("━━ *{}* ━━", period.label()));
lines.push(format!(
"{} tok · {} · {} sessions · {} calls",
fmt_tokens(data.summary.total_tokens),
fmt_cost(data.summary.total_cost),
format_number(data.summary.session_count),
format_number(data.summary.call_count),
));
if !data.daily.is_empty() && period != Period::Today {
lines.push(String::new());
lines.push("*Daily:*".to_string());
let window: Vec<_> = data.daily.iter().rev().take(7).collect();
for d in window.iter().rev() {
lines.push(format!(
" {} · {} tok · {}",
d.date,
fmt_tokens(d.tokens),
fmt_cost(d.cost)
));
}
}
if !data.models.is_empty() {
lines.push(String::new());
lines.push("*By Model:*".to_string());
for m in data.models.iter().take(5) {
let est = if m.estimated { " ~" } else { "" };
lines.push(format!(
" `{}` · {} tok · {}{}",
m.model,
fmt_tokens(m.tokens),
fmt_cost(m.cost),
est
));
}
}
if !data.tools.is_empty() {
lines.push(String::new());
lines.push("*Core Tools:*".to_string());
for t in data.tools.iter().take(5) {
lines.push(format!(
" `{}` · {} calls",
t.tool_name,
format_number(t.call_count)
));
}
}
if !data.projects.is_empty() {
lines.push(String::new());
lines.push("*By Project:*".to_string());
for p in data.projects.iter().take(5) {
lines.push(format!(
" `{}` · {} · {} sessions",
p.project,
fmt_cost(p.cost),
p.sessions
));
}
}
if !data.activities.is_empty() {
lines.push(String::new());
lines.push("*By Activity:*".to_string());
for a in data.activities.iter().take(5) {
lines.push(format!(
" {} · {} · {} turns · {:.0}% one-shot",
a.category,
fmt_cost(a.cost),
a.turns,
a.one_shot_pct,
));
}
}
lines.push(String::new());
}
lines.join("\n")
}
fn estimate_cost(model: &str, token_count: i64) -> Option<f64> {
crate::usage::pricing::PricingConfig::load()
.ok()
.and_then(|cfg| cfg.estimate_cost(model, token_count))
}
pub(crate) fn format_number(n: i64) -> String {
if n >= 1_000_000 {
format!("{:.1}M", n as f64 / 1_000_000.0)
} else if n >= 1_000 {
format!("{:.1}K", n as f64 / 1_000.0)
} else {
n.to_string()
}
}
async fn format_sessions(
current_session_id: Uuid,
session_svc: &SessionService,
) -> SessionsResponse {
let sessions = session_svc
.list_sessions(SessionListOptions {
include_archived: false,
limit: Some(10),
offset: 0,
})
.await
.unwrap_or_default();
let mut text_lines = vec!["📂 *Sessions*".to_string()];
let mut items = Vec::new();
let current = sessions.iter().find(|s| s.id == current_session_id);
if let Some(s) = current {
let title = s.title.as_deref().unwrap_or("Untitled");
text_lines.push(format!("Current: `{}`", title));
}
text_lines.push(String::new());
for s in &sessions {
let title = s.title.as_deref().unwrap_or("Untitled");
let date = s.updated_at.format("%b %d %H:%M");
let label = format!("{} ({})", title, date);
items.push((s.id, label));
}
if sessions.is_empty() {
text_lines.push("No sessions found.".to_string());
}
SessionsResponse {
current_session_id,
sessions: items,
text: text_lines.join("\n"),
}
}
fn format_providers(agent: &AgentService) -> ProvidersResponse {
let current_provider = agent.provider_name();
let current_model = agent.provider_model();
let providers = all_known_providers_with_status_loaded();
let mut text_lines = vec![
"🤖 *Switch Provider*".to_string(),
format!("Current: `{}` / `{}`", current_provider, current_model),
String::new(),
];
for (name, label, configured) in &providers {
let prefix = if !configured {
"🔒 "
} else if *name == current_provider {
"✓ "
} else {
"• "
};
text_lines.push(format!("{}`{}`", prefix, label));
}
text_lines.push(String::new());
text_lines.push("🔒 = needs API key (tap for setup steps)".to_string());
ProvidersResponse {
current_provider: current_provider.clone(),
current_model: current_model.clone(),
providers,
text: text_lines.join("\n"),
}
}
fn all_known_providers_with_status_loaded() -> Vec<(String, String, bool)> {
let config = match crate::config::Config::load() {
Ok(c) => c,
Err(_) => return vec![],
};
crate::utils::providers::all_known_providers_with_status(&config.providers)
}
pub fn unconfigured_provider_help(provider_name: &str) -> String {
let display = provider_display_name(provider_name);
let section = provider_name.replace("-", "_");
let path = crate::utils::providers::keys_toml_path_hint();
format!(
"🔒 *{display}* is not configured yet.\n\n\
To enable it, add this to `{path}`:\n\n\
```toml\n[providers.{section}]\napi_key = \"YOUR-{display}-KEY\"\n```\n\n\
Then restart OpenCrabs. Do NOT paste your API key here — \
Telegram keeps message history that bots cannot delete in DMs."
)
}
pub async fn models_for_provider(provider_name: &str) -> ModelsResponse {
let config = match crate::config::Config::load() {
Ok(c) => c,
Err(_) => {
return ModelsResponse {
provider_name: provider_name.to_string(),
current_model: String::new(),
models: vec![],
text: "Failed to load config.".to_string(),
agent_handled: false,
};
}
};
let display_name = provider_display_name(provider_name);
let config_models = provider_config_models(&config, provider_name);
let is_cli_provider = matches!(
provider_name,
"claude-cli" | "claude_cli" | "opencode-cli" | "opencode_cli"
);
if is_cli_provider {
let (canonical_models, canonical_default) =
crate::utils::providers::cli_supported_models(provider_name)
.unwrap_or_else(|| (Vec::new(), ""));
let current_model = config_models
.first()
.cloned()
.unwrap_or_else(|| canonical_default.to_string());
let models = if !config_models.is_empty() {
config_models
} else {
canonical_models
};
let mut text_lines = vec![
format!("🤖 *{} Models*", display_name),
format!("Current: `{}`", current_model),
String::new(),
];
for (i, m) in models.iter().enumerate() {
let marker = if *m == current_model { " ✓" } else { "" };
text_lines.push(format!("{}. `{}`{}", i + 1, m, marker));
}
return ModelsResponse {
provider_name: provider_name.to_string(),
current_model,
models,
text: text_lines.join("\n"),
agent_handled: false,
};
}
if provider_name == "openrouter" || provider_name.starts_with("custom:") {
let config_default = crate::utils::providers::config_for(&config.providers, provider_name)
.and_then(|c| c.default_model.clone());
let has_real_model = !config_models.is_empty() || config_default.is_some();
if !has_real_model {
let section = provider_name.replace(':', ".");
let path =
crate::utils::providers::keys_toml_path_hint().replace("keys.toml", "config.toml");
let text = format!(
"🤖 *{display_name} Models*\n\n\
No models configured for this provider.\n\n\
Add a `default_model` to `[providers.{section}]` in `{path}`, \
then restart OpenCrabs. Example:\n\n\
```toml\n[providers.{section}]\ndefault_model = \"YOUR-MODEL-NAME\"\n```",
);
return ModelsResponse {
provider_name: provider_name.to_string(),
current_model: String::new(),
models: vec![], text,
agent_handled: false,
};
}
let current_model = config_models
.first()
.cloned()
.or(config_default)
.expect("has_real_model guard ensures one of these is Some");
let models = if !config_models.is_empty() {
config_models
} else {
vec![current_model.clone()]
};
let mut text_lines = vec![
format!("🤖 *{} Models*", display_name),
format!("Current: `{}`", current_model),
String::new(),
];
for (i, m) in models.iter().enumerate() {
let marker = if *m == current_model { " ✓" } else { "" };
text_lines.push(format!("{}. `{}`{}", i + 1, m, marker));
}
return ModelsResponse {
provider_name: provider_name.to_string(),
current_model,
models,
text: text_lines.join("\n"),
agent_handled: false,
};
}
let provider = match crate::brain::provider::factory::create_provider_by_name(
&config,
provider_name,
)
.await
{
Ok(p) => p,
Err(e) => {
return ModelsResponse {
provider_name: provider_name.to_string(),
current_model: String::new(),
models: vec![],
text: format!("Failed to create provider: {}", e),
agent_handled: false,
};
}
};
let current_model = provider.default_model().to_string();
let mut models = if !config_models.is_empty() {
config_models
} else {
match tokio::time::timeout(std::time::Duration::from_secs(10), provider.fetch_models())
.await
{
Ok(fetched) if !fetched.is_empty() => fetched,
Ok(_) => vec![current_model.clone()],
Err(_) => {
tracing::warn!("fetch_models timed out for '{}'", provider_name);
vec![current_model.clone()]
}
}
};
if !models.contains(¤t_model) {
models.insert(0, current_model.clone());
}
let mut text_lines = vec![
format!("🤖 *{} Models*", display_name),
format!("Current: `{}`", current_model),
String::new(),
];
for (i, m) in models.iter().enumerate() {
let marker = if *m == current_model { " ✓" } else { "" };
text_lines.push(format!("{}. `{}`{}", i + 1, m, marker));
}
ModelsResponse {
provider_name: provider_name.to_string(),
current_model,
models,
text: text_lines.join("\n"),
agent_handled: false,
}
}
fn provider_config_models(config: &crate::config::Config, name: &str) -> Vec<String> {
crate::utils::providers::config_for(&config.providers, name)
.map(|c| c.models.clone())
.unwrap_or_default()
}
pub fn provider_display_name(name: &str) -> &str {
crate::utils::providers::display_name(name)
}
pub async fn switch_model(
agent: &AgentService,
model_name: &str,
session_id: Option<uuid::Uuid>,
provider_name_override: Option<&str>,
) -> Result<String, String> {
let provider_name = match provider_name_override {
Some(p) => p.to_string(),
None => match session_id {
Some(sid) => agent.provider_name_for_session(sid),
None => agent.provider_name(),
},
};
let config =
crate::config::Config::load().map_err(|e| format!("Failed to load config: {}", e))?;
tracing::info!(
"Channel: switched model to {} (provider: {}, session: {:?})",
model_name,
provider_name,
session_id
);
let new_provider =
crate::brain::provider::factory::create_provider_by_name(&config, &provider_name)
.await
.map_err(|e| {
tracing::warn!("Failed to create provider after model switch: {}", e);
format!("Model saved but failed to reload provider: {}", e)
})?;
let display_name = provider_display_name(&provider_name);
match session_id {
Some(sid) => {
agent.swap_provider_for_session(sid, new_provider, model_name.to_string());
}
None => agent.swap_provider(new_provider),
}
let change_msg = format!("[Model changed to {}/{}]", display_name, model_name);
if let Some(sid) = session_id {
let session_svc = crate::services::SessionService::new(agent.context().clone());
if let Ok(Some(mut session)) = session_svc.get_session(sid).await {
session.provider_name = Some(provider_name.clone());
session.model = Some(model_name.to_string());
if let Err(e) = session_svc.update_session(&session).await {
tracing::warn!("Failed to persist provider to session: {}", e);
}
}
let msg_svc = crate::services::MessageService::new(agent.context().clone());
if let Err(e) = msg_svc
.create_message(sid, "user".to_string(), change_msg.clone())
.await
{
tracing::warn!("Failed to persist model-change message: {}", e);
}
}
Ok(change_msg)
}
pub async fn run_evolve() -> String {
use crate::brain::agent::ProgressEvent;
use crate::brain::tools::{Tool, ToolExecutionContext, evolve::EvolveTool};
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
let restart_ready = Arc::new(AtomicBool::new(false));
let restart_flag = restart_ready.clone();
let progress_callback: crate::brain::agent::ProgressCallback = Arc::new(move |_sid, event| {
if matches!(event, ProgressEvent::RestartReady { .. }) {
restart_flag.store(true, Ordering::SeqCst);
}
});
let ctx = ToolExecutionContext::new(uuid::Uuid::nil());
let tool = EvolveTool::new(Some(progress_callback));
let result = match tool
.execute(serde_json::json!({"check_only": false}), &ctx)
.await
{
Ok(result) => result.output,
Err(e) => format!("Evolve failed: {}", e),
};
if restart_ready.load(Ordering::SeqCst) {
trigger_restart();
}
result
}
#[cfg(unix)]
fn trigger_restart() {
use std::os::unix::process::CommandExt;
let exe = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("opencrabs"));
let args: Vec<String> = std::env::args().skip(1).collect();
tracing::info!("Restarting daemon via exec()");
let err = std::process::Command::new(&exe).args(&args).exec();
tracing::error!("exec() failed: {}", err);
}
#[cfg(not(unix))]
fn trigger_restart() {
tracing::warn!("Restart via exec() not supported on this platform. Manual restart required.");
}
pub fn run_doctor() -> String {
use crate::brain::tools::slash_command::SlashCommandTool;
SlashCommandTool::doctor_text()
}
pub async fn try_execute_text_command(cmd: &ChannelCommand) -> Option<String> {
match cmd {
ChannelCommand::Help(body)
| ChannelCommand::Usage(body)
| ChannelCommand::UserSystem(body)
| ChannelCommand::Rtk(body) => Some(body.clone()),
ChannelCommand::Doctor => Some(run_doctor()),
ChannelCommand::Evolve => Some(run_evolve().await),
ChannelCommand::UnknownCommand(msg) => Some(msg.to_string()),
_ => None,
}
}
#[cfg(test)]
pub(crate) fn provider_section(provider_name: &str) -> Option<String> {
crate::utils::providers::config_section(provider_name)
}