use anyhow::{Context, Result, bail};
use clap::{Parser, Subcommand, ValueEnum};
use scv_protocol::{DaemonCommand, DaemonStatus, RemoteTools};
use scv_server::{ApprovalPolicy, ConfigOverrides};
use scv_tui::LaunchOptions;
use std::io::{self, IsTerminal, Write};
use std::path::{Path, PathBuf};
use std::process::{Command as ProcessCommand, Stdio};
#[derive(Parser)]
#[command(name = "scv", version, about = "SCV — Search, Construct, Verify")]
struct Cli {
#[arg(long, global = true, value_name = "PATH", env = "SCV_HOME")]
scv_home: Option<PathBuf>,
#[arg(long, global = true, value_name = "PATH", env = "SCV_CONFIG")]
config_path: Option<PathBuf>,
#[arg(long, global = true)]
model: Option<String>,
#[arg(long, global = true)]
provider: Option<String>,
#[arg(long, global = true)]
base_url: Option<String>,
#[arg(long, global = true, value_enum)]
approval_policy: Option<ApprovalArg>,
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Subcommand)]
enum Command {
Config {
#[command(subcommand)]
command: ConfigCommand,
},
Tui,
Exec {
prompt: String,
#[arg(long)]
yes: bool,
},
Run {
#[arg(long, value_name = "PATH", default_value = ".")]
workspace: PathBuf,
},
Start {
#[arg(long, value_name = "PATH", default_value = ".")]
workspace: PathBuf,
#[arg(long)]
allow_sudo: bool,
},
Stop,
Restart {
#[arg(long, value_name = "PATH", default_value = ".")]
workspace: PathBuf,
#[arg(long)]
allow_sudo: bool,
},
Status,
Reload,
Server {
#[arg(long, default_value_t = true)]
stdio: bool,
},
Update {
#[arg(long, value_name = "URL")]
index_url: Option<String>,
},
Clawbot {
#[command(subcommand)]
command: Option<ClawbotCommand>,
#[arg(long, default_value = "https://ilinkai.weixin.qq.com")]
base_url: String,
},
Agents {
#[command(subcommand)]
command: AgentsCommand,
},
ClawbotLogin {
#[arg(long, default_value = "https://ilinkai.weixin.qq.com")]
login_url: String,
},
}
#[derive(Subcommand)]
enum ConfigCommand {
Init,
}
#[derive(Subcommand)]
enum ClawbotCommand {
Login {
#[arg(long, default_value = "default")]
account: String,
#[arg(long, default_value = "https://ilinkai.weixin.qq.com")]
login_url: String,
},
Run {
#[arg(long, default_value = "default")]
account: String,
#[arg(long, default_value = ".")]
workspace: PathBuf,
#[arg(long, value_enum)]
remote_tools: Option<RemoteToolsArg>,
},
Stop {
#[arg(long, default_value = "default")]
account: String,
},
Status {
#[arg(long, default_value = "default")]
account: String,
},
Logout {
#[arg(long, default_value = "default")]
account: String,
},
}
#[derive(Subcommand)]
enum AgentsCommand {
Login {
#[arg(value_parser = agent_names())]
agent: String,
#[arg(long)]
openai_compatible: bool,
#[arg(long, requires = "openai_compatible")]
base_url: Option<String>,
#[arg(long, value_enum, requires = "openai_compatible")]
wire_api: Option<WireApiArg>,
#[arg(long, requires = "openai_compatible")]
model: Option<String>,
#[arg(last = true)]
extra: Vec<String>,
},
Status {
#[arg(value_parser = agent_names())]
agent: Option<String>,
},
Logout {
#[arg(value_parser = agent_names())]
agent: String,
},
Ps {
#[arg(long)]
all: bool,
},
Kill {
#[arg(required_unless_present = "orphans", conflicts_with = "orphans")]
handle: Option<String>,
#[arg(long)]
orphans: bool,
},
Import {
#[arg(value_parser = agent_names())]
agent: String,
#[arg(long, value_name = "DIR")]
from: Option<PathBuf>,
#[arg(long)]
from_scv_provider: bool,
},
}
fn agent_names() -> clap::builder::PossibleValuesParser {
clap::builder::PossibleValuesParser::new(
scv_server::adapters::ADAPTERS
.iter()
.map(|adapter| adapter.name),
)
}
fn agent_descriptor(name: &str) -> Result<&'static scv_server::adapters::AdapterDescriptor> {
scv_server::adapters::adapter(name).with_context(|| format!("unknown agent {name}"))
}
#[derive(Clone, Copy, ValueEnum)]
enum WireApiArg {
Responses,
Chat,
}
impl From<WireApiArg> for scv_server::WireApi {
fn from(value: WireApiArg) -> Self {
match value {
WireApiArg::Responses => Self::Responses,
WireApiArg::Chat => Self::ChatCompletions,
}
}
}
#[derive(Clone, Copy, ValueEnum)]
enum RemoteToolsArg {
None,
Owner,
}
impl From<RemoteToolsArg> for RemoteTools {
fn from(value: RemoteToolsArg) -> Self {
match value {
RemoteToolsArg::None => Self::None,
RemoteToolsArg::Owner => Self::Owner,
}
}
}
#[derive(Clone, Copy, ValueEnum)]
enum ApprovalArg {
OnRisk,
Always,
Never,
}
impl From<ApprovalArg> for ApprovalPolicy {
fn from(value: ApprovalArg) -> Self {
match value {
ApprovalArg::OnRisk => Self::OnRisk,
ApprovalArg::Always => Self::Always,
ApprovalArg::Never => Self::Never,
}
}
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
let cwd = std::env::current_dir()?;
apply_process_config(cli.scv_home.as_deref(), cli.config_path.as_deref(), &cwd)?;
let launch = LaunchOptions {
model: cli.model.clone(),
provider: cli.provider.clone(),
base_url: cli.base_url.clone(),
approval_policy: cli
.approval_policy
.map(|value| value.to_possible_value().unwrap().get_name().to_owned()),
};
let command = cli.command.unwrap_or(Command::Tui);
refuse_nested_daemon_control(&command)?;
match command {
Command::Config {
command: ConfigCommand::Init,
} => {
let path = scv_server::init_user_config()?;
println!("Created configuration at {}", path.display());
Ok(())
}
Command::Tui => scv_tui::run_tui(&cwd, launch).await,
Command::Exec { prompt, yes } => scv_tui::run_exec(&cwd, prompt, yes, launch).await,
Command::Server { stdio } => {
if !stdio {
anyhow::bail!("v0.1 supports only --stdio");
}
init_tracing();
scv_server::run_stdio(ConfigOverrides {
provider: cli.provider,
model: cli.model,
base_url: cli.base_url,
approval_policy: cli.approval_policy.map(Into::into),
no_tools: false,
})
.await
}
Command::Run { workspace } => {
init_tracing();
run_daemon(
&workspace,
ConfigOverrides {
provider: cli.provider,
model: cli.model,
base_url: cli.base_url,
approval_policy: cli.approval_policy.map(Into::into),
no_tools: false,
},
)
.await
}
Command::Start {
workspace,
allow_sudo,
} => daemon_control(
"start",
Some(&workspace),
cli.approval_policy,
cli.provider.as_deref(),
cli.model.as_deref(),
cli.base_url.as_deref(),
allow_sudo,
),
Command::Stop => daemon_control("stop", None, None, None, None, None, false),
Command::Restart {
workspace,
allow_sudo,
} => daemon_control(
"restart",
Some(&workspace),
cli.approval_policy,
cli.provider.as_deref(),
cli.model.as_deref(),
cli.base_url.as_deref(),
allow_sudo,
),
Command::Status => show_status(None).await,
Command::Reload => {
control(DaemonCommand::Reload).await?;
println!("Component configuration reloaded.");
Ok(())
}
Command::Update { index_url } => update_cli(&cwd, index_url),
Command::Clawbot { command, base_url } => match command.unwrap_or(ClawbotCommand::Login {
account: "default".into(),
login_url: base_url,
}) {
ClawbotCommand::Login { account, login_url } => {
clawbot_login(&login_url, &account).await
}
ClawbotCommand::Run {
account,
workspace,
remote_tools,
} => clawbot_run(&account, &workspace, remote_tools.map(Into::into)).await,
ClawbotCommand::Stop { account } => {
control(DaemonCommand::ClawbotSet {
account,
enabled: false,
workspace: None,
remote_tools: None,
})
.await?;
println!("ClawBot account disabled and stopped.");
Ok(())
}
ClawbotCommand::Status { account } => show_status(Some(&account)).await,
ClawbotCommand::Logout { account } => {
control(DaemonCommand::ClawbotLogout { account }).await?;
println!("ClawBot stopped; local credentials and delivery state removed.");
Ok(())
}
},
Command::Agents { command } => agents(command).await,
Command::ClawbotLogin { login_url } => clawbot_login(&login_url, "default").await,
}
}
fn refuse_nested_daemon_control(command: &Command) -> Result<()> {
let depth = scv_server::delegation::current_depth();
let lifecycle = match command {
Command::Run { .. } => Some("run"),
Command::Start { .. } => Some("start"),
Command::Stop => Some("stop"),
Command::Restart { .. } => Some("restart"),
Command::Update { .. } => Some("update"),
Command::Clawbot { .. } | Command::ClawbotLogin { .. } => Some("clawbot"),
_ => None,
};
if depth > 0
&& let Some(action) = lifecycle
{
bail!(
"`scv {action}` is refused inside a delegated agent run (delegation depth {depth}); \
the host owner manages the daemon"
);
}
Ok(())
}
fn update_cli(workspace: &Path, index_url: Option<String>) -> Result<()> {
let configured = scv_server::update_index_url(workspace)?;
let index_url = index_url
.or_else(|| std::env::var("SCV_CARGO_INDEX_URL").ok())
.or(configured);
let mut cargo = ProcessCommand::new("cargo");
cargo.args(["install", "--locked", "--force"]);
if let Some(index_url) = index_url.as_deref() {
cargo.args(["--index", index_url]);
println!("Updating SCV from Cargo index {index_url}");
} else {
println!("Updating SCV from crates.io");
}
cargo.arg("scv-cli");
let status = cargo.status().context("run cargo install for scv-cli")?;
if !status.success() {
bail!("SCV update failed while installing scv-cli");
}
let service = scv_server::service_name()?;
let active = ProcessCommand::new("systemctl")
.args(["--user", "is-active", "--quiet"])
.arg(&service)
.status()
.is_ok_and(|status| status.success());
if active {
let status = ProcessCommand::new("systemctl")
.args(["--user", "restart"])
.arg(&service)
.status()
.context("restart SCV daemon after update")?;
if !status.success() {
bail!("SCV updated, but restarting {service} failed");
}
println!("SCV updated and the running daemon was restarted; clients can reconnect.");
} else {
println!("SCV updated. No active user daemon was found to restart.");
}
Ok(())
}
fn daemon_control(
action: &str,
workspace: Option<&Path>,
approval_policy: Option<ApprovalArg>,
provider: Option<&str>,
model: Option<&str>,
base_url: Option<&str>,
allow_sudo: bool,
) -> Result<()> {
if action == "start" || action == "restart" {
ensure_sudo_expectation(allow_sudo)?;
}
if let Some(workspace) = workspace {
let workspace = std::fs::canonicalize(workspace).context("resolve daemon workspace")?;
let service = scv_server::service_name()?;
stop_legacy_instance(&service)?;
let path = scv_server::service_unit_path()?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let binary = std::env::current_exe()?;
let mut command = vec![
systemd_quote(binary.as_os_str()),
"run".into(),
"--workspace".into(),
systemd_quote(workspace.as_os_str()),
];
for (flag, value) in [
("--provider", provider),
("--model", model),
("--base-url", base_url),
] {
if let Some(value) = value {
command.push(flag.into());
command.push(systemd_quote(std::ffi::OsStr::new(value)));
}
}
if let Some(policy) = approval_policy {
command.push("--approval-policy".into());
command.push(systemd_quote(std::ffi::OsStr::new(
policy.to_possible_value().expect("value enum").get_name(),
)));
}
let instance_environment = std::env::var_os("SCV_HOME")
.map(|home| format!("Environment=SCV_HOME={}\n", systemd_quote(&home)))
.unwrap_or_default();
let config_environment = std::env::var_os("SCV_CONFIG")
.map(|config| format!("Environment=SCV_CONFIG={}\n", systemd_quote(&config)))
.unwrap_or_default();
let unit = format!(
"[Unit]\nDescription=SCV agent daemon\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nWorkingDirectory={}\nExecStart={}\nRestart=on-failure\nRestartSec=3\nEnvironment=RUST_LOG=info\n{}{}\n[Install]\nWantedBy=default.target\n",
systemd_path(workspace.as_os_str()),
command.join(" "),
instance_environment,
config_environment
);
write_atomic(&path, unit.as_bytes()).context("write SCV systemd unit")?;
}
let status = ProcessCommand::new("systemctl")
.args(["--user", "daemon-reload"])
.status()
.context("run systemctl")?;
if !status.success() {
bail!("systemctl daemon-reload failed");
}
let (verb, extra) = if action == "start" {
("enable", vec!["--now"])
} else {
(action, Vec::new())
};
let mut command = ProcessCommand::new("systemctl");
command.args(["--user", verb]);
command.args(extra);
command.arg(scv_server::service_name()?);
let status = command.status().context("run systemctl")?;
if !status.success() {
bail!("systemctl {action} {} failed", scv_server::service_name()?);
}
Ok(())
}
fn sudo_available() -> bool {
ProcessCommand::new("sudo")
.args(["-n", "-v"])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok_and(|status| status.success())
}
fn ensure_sudo_expectation(allow_sudo: bool) -> Result<()> {
if allow_sudo {
if !io::stdin().is_terminal() || !io::stderr().is_terminal() {
bail!(
"`scv start --allow-sudo` requires an interactive terminal so sudo can authenticate the current user"
);
}
let status = ProcessCommand::new("sudo")
.arg("-v")
.status()
.context("check sudo authorization")?;
if !status.success() {
bail!(
"sudo authorization failed; SCV cannot grant sudo access. Ask an administrator to add your user to the system sudo policy."
);
}
return Ok(());
}
if sudo_available() {
return Ok(());
}
let warning = "SCV is starting without verified sudo authorization. The daemon will run as your user, and commands requiring sudo may fail. Continue? [Y/n] ";
if !io::stdin().is_terminal() || !io::stderr().is_terminal() {
bail!(
"SCV has no verified sudo authorization. Re-run interactively to confirm continuing, or use `scv start --allow-sudo` to authenticate the current user's existing sudo rights."
);
}
eprint!("{warning}");
io::stderr().flush().context("flush sudo warning")?;
let mut answer = String::new();
io::stdin()
.read_line(&mut answer)
.context("read sudo warning response")?;
if matches!(answer.trim().to_ascii_lowercase().as_str(), "n" | "no") {
bail!("SCV start cancelled because sudo authorization was not verified.");
}
Ok(())
}
fn apply_process_config(home: Option<&Path>, config: Option<&Path>, cwd: &Path) -> Result<()> {
if let Some(home) = home {
let home = absolute_path(home, cwd);
std::fs::create_dir_all(&home).context("create SCV instance home")?;
let home = std::fs::canonicalize(home).context("resolve SCV instance home")?;
unsafe { std::env::set_var("SCV_HOME", home) };
}
if let Some(config) = config {
let config = absolute_path(config, cwd);
unsafe { std::env::set_var("SCV_CONFIG", config) };
}
Ok(())
}
fn absolute_path(path: &Path, cwd: &Path) -> PathBuf {
if path.is_absolute() {
path.to_path_buf()
} else {
cwd.join(path)
}
}
fn systemd_quote(value: &std::ffi::OsStr) -> String {
let value = value.to_string_lossy();
format!(
"\"{}\"",
value
.replace('%', "%%")
.replace('\\', "\\\\")
.replace('"', "\\\"")
)
}
fn systemd_path(value: &std::ffi::OsStr) -> String {
let value = value.to_string_lossy();
let mut encoded = String::with_capacity(value.len());
for character in value.chars() {
match character {
'%' => encoded.push_str("%%"),
'\\' => encoded.push_str("\\\\"),
'"' => encoded.push_str("\\\""),
'\t' => encoded.push_str("\\x09"),
'\n' => encoded.push_str("\\x0a"),
' ' => encoded.push_str("\\x20"),
_ => encoded.push(character),
}
}
encoded
}
fn write_atomic(path: &Path, contents: &[u8]) -> Result<()> {
let parent = path
.parent()
.ok_or_else(|| anyhow::anyhow!("path has no parent"))?;
let mut temporary =
tempfile::NamedTempFile::new_in(parent).context("create temporary systemd unit")?;
temporary
.write_all(contents)
.context("write temporary systemd unit")?;
temporary
.as_file()
.sync_all()
.context("sync temporary systemd unit")?;
temporary
.persist(path)
.map(|_| ())
.map_err(|error| error.error)
.context("install systemd unit")
}
fn stop_legacy_instance(service: &str) -> Result<()> {
if service == "scv.service" {
return Ok(());
}
let Some(home) = std::env::var_os("SCV_HOME") else {
return Ok(());
};
let expected_home = format!("SCV_HOME={}", PathBuf::from(home).display());
let output = ProcessCommand::new("systemctl")
.args([
"--user",
"show",
"scv.service",
"-p",
"Environment",
"--value",
])
.output()
.context("inspect legacy SCV service")?;
if output.status.success()
&& String::from_utf8_lossy(&output.stdout)
.split_whitespace()
.any(|entry| entry == expected_home)
{
let status = ProcessCommand::new("systemctl")
.args(["--user", "disable", "--now", "scv.service"])
.status()
.context("stop legacy SCV service")?;
if !status.success() {
bail!("failed to stop legacy scv.service for this SCV profile");
}
}
Ok(())
}
async fn run_daemon(workspace: &Path, overrides: ConfigOverrides) -> Result<()> {
let socket = scv_server::default_socket_path()?;
std::env::set_current_dir(workspace)
.with_context(|| format!("change to daemon workspace {}", workspace.display()))?;
scv_server::run_socket(&socket, overrides).await
}
async fn agents(command: AgentsCommand) -> Result<()> {
use scv_server::adapters::{Login, Logout, Status};
match command {
AgentsCommand::Login {
agent,
openai_compatible,
base_url,
wire_api,
model,
extra,
} => {
let adapter = agent_descriptor(&agent)?;
let name = adapter.name;
if openai_compatible {
if name != "pi" {
bail!(
"--openai-compatible configures pi; {name} signs in with `scv agents login {name}`"
);
}
let endpoint = scv_server::Endpoint {
base_url: match base_url {
Some(url) => url,
None => prompt_line("Base URL (e.g. https://host/v1)")?,
},
api: match wire_api {
Some(api) => api.into(),
None => match prompt_line("Wire API [responses/chat] (default responses)")?
.as_str()
{
"" | "responses" => scv_server::WireApi::Responses,
"chat" => scv_server::WireApi::ChatCompletions,
other => bail!("unknown wire API {other:?}; use responses or chat"),
},
},
model: match model {
Some(model) => model,
None => prompt_line("Default model id")?,
},
};
let key = scv_server::read_secret("API key (input hidden)")?;
for line in scv_server::configure_pi_endpoint(&endpoint, &key)? {
println!("{line}");
}
println!("Check with `scv agents status pi`.");
return Ok(());
}
println!(
"Signing {name} in for SCV's agent_{name} tool (separate from your own {} login).",
adapter.product
);
match adapter.login {
Login::Command(args) => run_agent(name, args, &extra, "sign-in")?,
Login::Interactive { args, hint } => {
println!("Opening {} in SCV's adapter home: {hint}.", adapter.product);
run_agent(name, args, &extra, "sign-in")?;
}
Login::ApiKey(store) => {
if !extra.is_empty() {
bail!("{name} takes its API key from a prompt or stdin, not arguments");
}
let key = scv_server::read_secret(&format!(
"{} API key (input hidden)",
adapter.product
))?;
for line in scv_server::store_agent_key(name, store, &key)? {
println!("{line}");
}
}
}
println!(
"Done. `scv agents status` shows the result; the daemon picks it up on the next call."
);
Ok(())
}
AgentsCommand::Status { agent } => {
let selected: Vec<_> = match agent {
Some(name) => vec![agent_descriptor(&name)?],
None => scv_server::adapters::ADAPTERS.iter().collect(),
};
for adapter in selected {
let name = adapter.name;
println!("{name}:");
let installed = scv_server::agent_executable(name)?;
if installed.is_none() {
println!(
" not installed ({:?} is not on PATH or in ~/.local/bin)",
adapter.command
);
}
match adapter.status {
Status::Command(args) if installed.is_some() => {
match scv_server::agent_command(name)?
.args(args)
.stdin(std::process::Stdio::null())
.output()
{
Ok(output) => {
let summary = scv_server::adapters::summarize_status(
adapter.status_summary,
output.status.success(),
&String::from_utf8_lossy(&output.stdout),
);
println!(" {summary}");
if summary == "not signed in" {
println!(" Sign in for SCV with: scv agents login {name}");
}
}
Err(error) => println!(" unavailable: {error}"),
}
}
Status::Command(_) => {}
Status::Stored(store) => {
let (ready, lines) = scv_server::agent_stored_status(name, store)?;
for line in lines {
println!(" {line}");
}
if !ready {
println!(" Sign in for SCV with: scv agents login {name}");
}
}
}
}
Ok(())
}
AgentsCommand::Ps { all } => {
let status = control(DaemonCommand::Delegations { all }).await?;
print_delegations(&status.delegations.entries);
Ok(())
}
AgentsCommand::Kill { handle, orphans } => {
let status = control(DaemonCommand::DelegationKill { handle, orphans }).await?;
if status.delegations.killed.is_empty() {
println!("Nothing to stop.");
} else {
println!("Stopped: {}", status.delegations.killed.join(", "));
}
Ok(())
}
AgentsCommand::Import {
agent,
from,
from_scv_provider,
} => match agent.as_str() {
"codex" => {
if from_scv_provider {
bail!("--from-scv-provider applies to pi; codex imports your own Codex home");
}
let source = from
.or_else(|| std::env::var_os("CODEX_HOME").map(PathBuf::from))
.or_else(|| {
std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".codex"))
})
.context("cannot determine your Codex home; pass --from")?;
println!("Importing Codex setup from {}", source.display());
for line in scv_server::import_codex(&source)? {
println!(" {line}");
}
println!(
"This is a copy: re-run after changing your own Codex config. Check with `scv agents status codex`."
);
Ok(())
}
"pi" => {
if !from_scv_provider || from.is_some() {
bail!(
"pi imports SCV's own provider: `scv agents import pi --from-scv-provider`"
);
}
println!("Pointing SCV's pi at SCV's own provider");
for line in scv_server::import_pi_from_scv_provider()? {
println!(" {line}");
}
println!(
"This is a copy: re-run after changing SCV's provider. Check with `scv agents status pi`."
);
Ok(())
}
other => {
bail!("{other} has nothing to import; sign it in with `scv agents login {other}`")
}
},
AgentsCommand::Logout { agent } => {
let adapter = agent_descriptor(&agent)?;
match adapter.logout {
Logout::Command(args) => run_agent(adapter.name, args, &[], "sign-out"),
Logout::Stored(store) => {
for line in scv_server::remove_agent_credentials(adapter.name, store)? {
println!("{line}");
}
Ok(())
}
}
}
}
}
fn run_agent(name: &str, args: &[&str], extra: &[String], action: &str) -> Result<()> {
let status = scv_server::agent_command(name)?
.args(args)
.args(extra)
.status()
.with_context(|| format!("run {name} {action}"))?;
if !status.success() {
bail!("{name} {action} did not complete");
}
Ok(())
}
fn prompt_line(prompt: &str) -> Result<String> {
use std::io::{BufRead as _, IsTerminal as _, Write as _};
if std::io::stdin().is_terminal() {
eprint!("{prompt}: ");
std::io::stderr().flush().ok();
}
let mut line = String::new();
std::io::stdin().lock().read_line(&mut line)?;
Ok(line.trim().to_owned())
}
async fn clawbot_run(
account: &str,
workspace: &Path,
remote_tools: Option<RemoteTools>,
) -> Result<()> {
let workspace = std::fs::canonicalize(workspace).context("resolve ClawBot workspace")?;
let status = control(DaemonCommand::ClawbotSet {
account: account.into(),
enabled: true,
workspace: Some(workspace.display().to_string()),
remote_tools,
})
.await?;
println!(
"ClawBot enabled under the SCV daemon; use `scv clawbot status` for live connection state."
);
if remote_tools == Some(RemoteTools::Owner) {
let effective = status
.components
.iter()
.any(|health| health.account == account && health.remote_tools == RemoteTools::Owner);
if effective {
println!(
"Remote tools: the bot's own WeChat account now runs every SCV tool without approval prompts."
);
} else {
println!(
"Warning: owner remote tools are saved but not active; the daemon may predate them or the login lacks an owner ID. Remote sessions stay tool-free."
);
}
}
Ok(())
}
fn print_delegations(entries: &[scv_protocol::DelegationInfo]) {
if entries.is_empty() {
println!("No delegated agent runs.");
return;
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |elapsed| elapsed.as_secs());
println!(
"{:<16} {:<7} {:<9} {:>8} {:>5} {:>7} {:>5} CWD",
"HANDLE", "AGENT", "STATE", "PID", "PROCS", "AGE", "DEPTH"
);
for entry in entries {
let age = now.saturating_sub(entry.started_unix_seconds);
println!(
"{:<16} {:<7} {:<9} {:>8} {:>5} {:>7} {:>5} {:?}",
entry.handle,
entry.agent,
if entry.orphaned {
"orphaned"
} else {
"running"
},
entry.pid,
entry.processes,
format!("{}m{:02}s", age / 60, age % 60),
entry.depth,
entry.cwd,
);
}
}
async fn control(command: DaemonCommand) -> Result<DaemonStatus> {
scv_client::control(&scv_client::default_socket_path()?, command).await
}
async fn clawbot_login(login_url: &str, account: &str) -> Result<()> {
scv_clawbot::login(login_url, account).await?;
match control(DaemonCommand::Reload).await {
Ok(_) => println!("Daemon refreshed; enabled accounts start automatically."),
Err(_) => println!(
"Credentials saved. The daemon will load enabled accounts at startup or its next refresh."
),
}
Ok(())
}
async fn show_status(account: Option<&str>) -> Result<()> {
let status = match control(DaemonCommand::Status).await {
Ok(status) => status,
Err(error) => {
println!("Daemon: unavailable; component connectivity is unknown.");
return Err(error);
}
};
println!(
"Daemon: running, version {}, pid {}",
status.version, status.pid
);
println!(
"Delegations: {} running, {} orphaned runs stopped since the daemon started",
status.delegations.active, status.delegations.reaped
);
for health in status
.components
.iter()
.filter(|h| account.is_none_or(|name| h.account == name))
{
println!("{}", serde_json::to_string(health)?);
}
if status
.components
.iter()
.all(|h| account.is_some_and(|name| h.account != name))
{
println!("No matching supervised components.");
}
Ok(())
}
fn init_tracing() {
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn"));
let _ = tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.try_init();
}