fix: stabilize daemon service lifecycle and align Agent Status UI (#6)

* Update Welcome page to integrate OAuth provider and adjust branding

- Added OAuthProviderButton to the Welcome component for Google authentication.
- Updated the branding from AlphaHuman to OpenHuman in the FeaturesStep component.
- Removed unnecessary Lottie animation from the Onboarding page for a cleaner layout.
- Changed the backend URL to point to the new API endpoint for TinyHumans.

* Refactor routing and sidebar components for improved clarity

- Commented out the Mnemonic route and related logic in AppRoutes for future consideration.
- Updated MiniSidebar to comment out the Invite Friends section.
- Removed unused navigation logic and upgrade call-to-action from the Home page.
- Adjusted Onboarding navigation to redirect to Home instead of Mnemonic.

* Comment out unused settings menu items for future consideration in SettingsHome component

* Add core process and RPC functionality for Alphahuman

- Introduced new binaries: `alphahuman-core` and `alphahuman-cli` for core process management and command-line interaction.
- Implemented `CoreProcessHandle` for managing the lifecycle of the core process.
- Added `core_rpc` module for handling RPC requests and responses.
- Created `core_server` module to manage core server logic and routing.
- Updated `alphahuman` commands to utilize the new RPC structure for health checks, security policies, and configuration management.
- Refactored existing code to streamline interactions with the core process and improve overall architecture.

* Update dependencies and enhance CLI functionality

- Added `clap` for command-line argument parsing in the `alphahuman-cli`.
- Updated `Cargo.lock` and `Cargo.toml` to include `clap` and its features.
- Refactored `alphahuman-cli` to utilize structured command handling with subcommands.
- Improved core process management and logging in `core_process.rs`.
- Enhanced routing and error handling in `core_server.rs` with new root and not found handlers.
- Updated various commands in `alphahuman.rs` to ensure core process is running before executing RPC calls.

* Enhance service management and CLI functionality

- Added new functions for daemon program arguments and command line construction in the service module.
- Updated macOS and Linux installation functions to dynamically generate program arguments and command lines.
- Introduced a new `Reinstall` command in the CLI for easier service management.
- Refactored service command handling to utilize local service functions for improved clarity and maintainability.

* Refactor daemon references to agent in components and hooks

- Updated terminology from "Daemon" to "Agent" in MiniSidebar, DaemonHealthPanel, and TauriCommandsPanel for consistency.
- Modified useDaemonHealth hook to probe agent status and handle agent lifecycle management.
- Introduced new agent server status interface and function in Tauri commands for improved status checking.
- Adjusted related comments and error handling to reflect the changes in terminology.

* Refactor Home component and enhance macOS service management

- Updated Home component to navigate to in-app conversations instead of opening a Telegram bot link.
- Improved macOS service management by implementing modern lifecycle commands and adding compatibility fallbacks for service control.
- Introduced new utility functions for handling macOS GUI domain and service targets.

* Refactor Home component to enhance navigation

- Removed SkillsGrid component and replaced it with a button that navigates to the Skills page.
- Improved layout with additional margin for better spacing in the Home component.
This commit is contained in:
Steven Enamakel
2026-03-24 21:04:19 -07:00
committed by GitHub
parent 248921606e
commit 37318bc73d
25 changed files with 2033 additions and 696 deletions
+48 -8
View File
@@ -17,6 +17,7 @@ dependencies = [
"chacha20poly1305",
"chrono",
"chrono-tz",
"clap",
"console",
"cron",
"dialoguer",
@@ -1138,6 +1139,46 @@ dependencies = [
"zeroize",
]
[[package]]
name = "clap"
version = "4.5.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "cmake"
version = "0.1.57"
@@ -2177,7 +2218,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -6349,7 +6390,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.52.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -7016,7 +7057,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -7029,7 +7070,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.11.0",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -7585,7 +7626,7 @@ version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb"
dependencies = [
"dirs 4.0.0",
"dirs 6.0.0",
]
[[package]]
@@ -7736,7 +7777,6 @@ dependencies = [
"cfg-if",
"libc",
"psm",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
@@ -8312,7 +8352,7 @@ dependencies = [
"getrandom 0.3.4",
"once_cell",
"rustix 1.1.3",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -9941,7 +9981,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.61.2",
]
[[package]]
+9
View File
@@ -23,6 +23,14 @@ path = "src/main.rs"
name = "alphahuman-tools-discovery"
path = "src/bin/alphahuman-tools-discovery.rs"
[[bin]]
name = "alphahuman-core"
path = "src/bin/alphahuman-core.rs"
[[bin]]
name = "alphahuman-cli"
path = "src/bin/alphahuman-cli.rs"
[build-dependencies]
tauri-build = { version = "2", features = [] }
serde = { version = "1", features = ["derive"] }
@@ -119,6 +127,7 @@ rustls = { version = "0.23", features = ["ring"] }
rustls-pki-types = "1.14.0"
tokio-rustls = "0.26.4"
webpki-roots = "1.0.6"
clap = { version = "4.5", features = ["derive"] }
lettre = { version = "0.11.19", default-features = false, features = ["builder", "smtp-transport", "rustls-tls"] }
mail-parser = "0.11.2"
async-imap = { version = "0.11", features = ["runtime-tokio"], default-features = false }
+158 -28
View File
@@ -5,7 +5,7 @@ use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use std::process::{Command, Stdio};
const SERVICE_LABEL: &str = "com.alphahuman.daemon";
const LEGACY_SERVICE_LABEL: &str = "com.alphahuman.app";
@@ -15,6 +15,30 @@ fn windows_task_name() -> &'static str {
WINDOWS_TASK_NAME
}
fn daemon_program_args(exe: &std::path::Path) -> Vec<String> {
let file_name = exe
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default()
.to_ascii_lowercase();
if file_name.contains("alphahuman-core") {
vec!["serve".to_string()]
} else {
vec!["core".to_string(), "serve".to_string()]
}
}
fn daemon_command_line(exe: &std::path::Path) -> String {
let args = daemon_program_args(exe);
let exe_quoted = format!("\"{}\"", exe.display());
if args.is_empty() {
exe_quoted
} else {
format!("{} {}", exe_quoted, args.join(" "))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ServiceState {
Running,
@@ -49,19 +73,39 @@ pub fn install(config: &Config) -> Result<ServiceStatus> {
pub fn start(config: &Config) -> Result<ServiceStatus> {
if cfg!(target_os = "macos") {
let plist = macos_service_file()?;
let domain = macos_gui_domain()?;
let primary_target = macos_target(SERVICE_LABEL)?;
// Check if service is already loaded to avoid "Input/output error"
// Prefer modern launchctl lifecycle commands on macOS.
if !is_service_loaded_macos()? {
log::info!("[service] Loading macOS LaunchAgent service");
run_checked(Command::new("launchctl").arg("load").arg("-w").arg(&plist))?;
let bootstrap_ok = run_checked(
Command::new("launchctl")
.arg("bootstrap")
.arg(&domain)
.arg(&plist),
);
if let Err(err) = bootstrap_ok {
log::warn!(
"[service] launchctl bootstrap failed, falling back to load -w: {err}"
);
run_checked(Command::new("launchctl").arg("load").arg("-w").arg(&plist))?;
}
} else {
log::info!("[service] LaunchAgent service already loaded, skipping load step");
}
// Always try to start - this is safe even if already running
log::info!("[service] Starting macOS LaunchAgent service");
let start_result = run_checked(Command::new("launchctl").arg("start").arg(SERVICE_LABEL));
let start_result = run_checked(
Command::new("launchctl")
.arg("kickstart")
.arg("-k")
.arg(&primary_target),
);
if let Err(e) = start_result {
log::warn!("[service] launchctl kickstart failed, trying launchctl start");
let _ = run_checked(Command::new("launchctl").arg("start").arg(SERVICE_LABEL));
// Check if it's already running - that's not an error for us
let status_check = status(config)?;
if matches!(status_check.state, ServiceState::Running) {
@@ -77,7 +121,11 @@ pub fn start(config: &Config) -> Result<ServiceStatus> {
// Check if service is enabled before trying to start
if !is_service_enabled_linux()? {
log::info!("[service] Enabling systemd service");
let _ = run_checked(Command::new("systemctl").args(["--user", "enable", "alphahuman.service"]));
let _ = run_checked(Command::new("systemctl").args([
"--user",
"enable",
"alphahuman.service",
]));
} else {
log::info!("[service] Systemd service already enabled");
}
@@ -86,7 +134,8 @@ pub fn start(config: &Config) -> Result<ServiceStatus> {
// Try to start - systemctl start is idempotent
log::info!("[service] Starting systemd service");
let start_result = run_checked(Command::new("systemctl").args(["--user", "start", "alphahuman.service"]));
let start_result =
run_checked(Command::new("systemctl").args(["--user", "start", "alphahuman.service"]));
if let Err(e) = start_result {
// Check if it's already active - that's success for us
let status_check = status(config)?;
@@ -134,17 +183,47 @@ pub fn start(config: &Config) -> Result<ServiceStatus> {
pub fn stop(config: &Config) -> Result<ServiceStatus> {
if cfg!(target_os = "macos") {
let plist = macos_service_file()?;
let _ = run_checked(Command::new("launchctl").arg("stop").arg(SERVICE_LABEL));
let _ = run_checked(Command::new("launchctl").arg("unload").arg("-w").arg(&plist));
let domain = macos_gui_domain()?;
let primary_target = macos_target(SERVICE_LABEL)?;
let legacy_plist = macos_service_file_for(LEGACY_SERVICE_LABEL)?;
let _ = run_checked(Command::new("launchctl").arg("stop").arg(LEGACY_SERVICE_LABEL));
let _ = run_checked(Command::new("launchctl").arg("unload").arg("-w").arg(&legacy_plist));
let legacy_target = macos_target(LEGACY_SERVICE_LABEL)?;
// Modern lifecycle path first.
run_best_effort(
Command::new("launchctl")
.arg("bootout")
.arg(&domain)
.arg(&primary_target),
);
run_best_effort(
Command::new("launchctl")
.arg("bootout")
.arg(&domain)
.arg(&plist),
);
run_best_effort(
Command::new("launchctl")
.arg("bootout")
.arg(&domain)
.arg(&legacy_target),
);
run_best_effort(
Command::new("launchctl")
.arg("bootout")
.arg(&domain)
.arg(&legacy_plist),
);
// Compatibility fallback.
run_best_effort(Command::new("launchctl").arg("stop").arg(SERVICE_LABEL));
run_best_effort(Command::new("launchctl").arg("stop").arg(LEGACY_SERVICE_LABEL));
return status(config);
}
if cfg!(target_os = "linux") {
let _ = run_checked(Command::new("systemctl").args(["--user", "stop", "alphahuman.service"]));
let _ =
run_checked(Command::new("systemctl").args(["--user", "stop", "alphahuman.service"]));
return status(config);
}
@@ -161,9 +240,9 @@ pub fn stop(config: &Config) -> Result<ServiceStatus> {
pub fn status(config: &Config) -> Result<ServiceStatus> {
if cfg!(target_os = "macos") {
let out = run_capture(Command::new("launchctl").arg("list"))?;
let running = out.lines().any(|line| {
line.contains(SERVICE_LABEL) || line.contains(LEGACY_SERVICE_LABEL)
});
let running = out
.lines()
.any(|line| line.contains(SERVICE_LABEL) || line.contains(LEGACY_SERVICE_LABEL));
return Ok(ServiceStatus {
state: if running {
ServiceState::Running
@@ -199,7 +278,8 @@ pub fn status(config: &Config) -> Result<ServiceStatus> {
if cfg!(target_os = "windows") {
let _ = config;
let task_name = windows_task_name();
let out = run_capture(Command::new("schtasks").args(["/Query", "/TN", task_name, "/FO", "LIST"]));
let out =
run_capture(Command::new("schtasks").args(["/Query", "/TN", task_name, "/FO", "LIST"]));
match out {
Ok(text) => {
let running = text.contains("Running");
@@ -304,6 +384,11 @@ fn install_macos(config: &Config) -> Result<()> {
let stdout = logs_dir.join("daemon.stdout.log");
let stderr = logs_dir.join("daemon.stderr.log");
let daemon_args = daemon_program_args(&exe);
let program_args_xml = std::iter::once(exe.display().to_string())
.chain(daemon_args)
.map(|arg| format!(" <string>{}</string>\n", xml_escape(&arg)))
.collect::<String>();
let plist = format!(
r#"<?xml version=\"1.0\" encoding=\"UTF-8\"?>
@@ -314,9 +399,7 @@ fn install_macos(config: &Config) -> Result<()> {
<string>{label}</string>
<key>ProgramArguments</key>
<array>
<string>{exe}</string>
<string>daemon</string>
</array>
{program_args} </array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
@@ -330,13 +413,25 @@ fn install_macos(config: &Config) -> Result<()> {
<key>ALPHAHUMAN_DAEMON_INTERNAL</key>
<string>false</string>
</dict>
<key>WorkingDirectory</key>
<string>{workdir}</string>
<key>ProcessType</key>
<string>Background</string>
</dict>
</plist>
"#,
label = SERVICE_LABEL,
exe = xml_escape(&exe.display().to_string()),
program_args = program_args_xml,
stdout = xml_escape(&stdout.display().to_string()),
stderr = xml_escape(&stderr.display().to_string())
stderr = xml_escape(&stderr.display().to_string()),
workdir = xml_escape(
&config
.config_path
.parent()
.map_or_else(|| PathBuf::from("."), PathBuf::from)
.display()
.to_string(),
)
);
fs::write(&file, plist)?;
@@ -359,10 +454,11 @@ fn install_linux(config: &Config) -> Result<()> {
let stdout = logs_dir.join("daemon.stdout.log");
let stderr = logs_dir.join("daemon.stderr.log");
let exec_start = daemon_command_line(&exe);
let unit = format!(
"[Unit]\nDescription=Alphahuman Daemon\n\n[Service]\nExecStart={} daemon\nRestart=always\nRestartSec=3\n\nStandardOutput=append:{}\nStandardError=append:{}\n\n[Install]\nWantedBy=default.target\n",
exe.display(),
"[Unit]\nDescription=Alphahuman Daemon\n\n[Service]\nExecStart={}\nRestart=always\nRestartSec=3\n\nStandardOutput=append:{}\nStandardError=append:{}\n\n[Install]\nWantedBy=default.target\n",
exec_start,
stdout.display(),
stderr.display(),
);
@@ -384,10 +480,11 @@ fn install_windows(config: &Config) -> Result<()> {
let wrapper = logs_dir.join("alphahuman-daemon.cmd");
let stdout = logs_dir.join("daemon.stdout.log");
let stderr = logs_dir.join("daemon.stderr.log");
let daemon_cmd = daemon_command_line(&exe);
let cmd = format!(
"@echo off\n\"{}\" daemon >> \"{}\" 2>> \"{}\"\n",
exe.display(),
"@echo off\n{} >> \"{}\" 2>> \"{}\"\n",
daemon_cmd,
stdout.display(),
stderr.display()
);
@@ -428,6 +525,15 @@ fn macos_service_file_for(label: &str) -> Result<PathBuf> {
.join(format!("{label}.plist")))
}
fn macos_gui_domain() -> Result<String> {
let uid = run_capture(Command::new("id").arg("-u"))?;
Ok(format!("gui/{}", uid.trim()))
}
fn macos_target(label: &str) -> Result<String> {
Ok(format!("{}/{}", macos_gui_domain()?, label))
}
fn linux_service_file(config: &Config) -> Result<PathBuf> {
let config_dir = config
.config_path
@@ -458,12 +564,36 @@ fn run_capture(cmd: &mut Command) -> Result<String> {
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
fn run_best_effort(cmd: &mut Command) {
match cmd.stdout(Stdio::null()).stderr(Stdio::null()).status() {
Ok(status) => {
if !status.success() {
log::debug!("[service] best-effort command failed with status {status}");
}
}
Err(err) => {
log::debug!("[service] best-effort command failed to execute: {err}");
}
}
}
/// Check if the macOS LaunchAgent service is loaded (regardless of running state)
fn is_service_loaded_macos() -> Result<bool> {
let out = run_capture(Command::new("launchctl").arg("list"))?;
Ok(out.lines().any(|line| {
line.contains(SERVICE_LABEL) || line.contains(LEGACY_SERVICE_LABEL)
}))
if run_checked(Command::new("launchctl").arg("print").arg(macos_target(SERVICE_LABEL)?))
.is_ok()
{
return Ok(true);
}
if run_checked(
Command::new("launchctl")
.arg("print")
.arg(macos_target(LEGACY_SERVICE_LABEL)?),
)
.is_ok()
{
return Ok(true);
}
Ok(false)
}
/// Check if the Linux systemd service is enabled
+390
View File
@@ -0,0 +1,390 @@
use clap::{Args, Parser, Subcommand};
use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Debug, Serialize)]
struct RpcRequest {
jsonrpc: &'static str,
id: u64,
method: String,
params: serde_json::Value,
}
#[derive(Debug, Deserialize)]
struct RpcResponse {
result: Option<serde_json::Value>,
error: Option<RpcError>,
}
#[derive(Debug, Deserialize)]
struct RpcError {
code: i64,
message: String,
data: Option<serde_json::Value>,
}
#[derive(Debug, Parser)]
#[command(name = "alphahuman-cli")]
#[command(about = "CLI for the AlphaHuman core RPC server")]
struct Cli {
/// Core RPC endpoint URL
#[arg(long, global = true)]
rpc_url: Option<String>,
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
enum Command {
/// Check core health
Ping,
/// Print core version
Version,
/// Get health snapshot
Health,
/// Get runtime flags
RuntimeFlags,
/// Config operations
Config {
#[command(subcommand)]
command: ConfigCommand,
},
/// Service operations
Service {
#[command(subcommand)]
command: ServiceCommand,
},
/// Doctor operations
Doctor {
#[command(subcommand)]
command: DoctorCommand,
},
/// Integrations operations
Integrations {
#[command(subcommand)]
command: IntegrationsCommand,
},
/// Send one-shot agent message
AgentChat(AgentChatArgs),
/// Hardware operations
Hardware {
#[command(subcommand)]
command: HardwareCommand,
},
/// Encrypt a secret
Encrypt {
plaintext: String,
},
/// Decrypt a secret
Decrypt {
ciphertext: String,
},
/// Toggle browser allow-all runtime flag
BrowserAllowAll {
#[arg(long)]
enabled: bool,
},
/// Refresh model catalog
ModelsRefresh {
#[arg(long)]
provider: Option<String>,
#[arg(long, default_value_t = false)]
force: bool,
},
/// Migrate OpenClaw memory
MigrateOpenclaw {
#[arg(long)]
source_workspace: Option<String>,
#[arg(long, default_value_t = true)]
dry_run: bool,
},
}
#[derive(Debug, Subcommand)]
enum ConfigCommand {
/// Get full config snapshot
Get,
}
#[derive(Debug, Subcommand)]
enum ServiceCommand {
Install,
Start,
Stop,
Status,
Reinstall,
Uninstall,
}
#[derive(Debug, Subcommand)]
enum DoctorCommand {
/// Run doctor checks
Report,
/// Probe model catalog
Models {
#[arg(long)]
provider: Option<String>,
#[arg(long, default_value_t = true)]
use_cache: bool,
},
}
#[derive(Debug, Subcommand)]
enum IntegrationsCommand {
/// List integrations
List,
/// Get one integration info
Info {
#[arg(long)]
name: String,
},
}
#[derive(Debug, Subcommand)]
enum HardwareCommand {
/// Discover connected hardware
Discover,
/// Introspect one device path
Introspect {
#[arg(long)]
path: String,
},
}
#[derive(Debug, Args)]
struct AgentChatArgs {
message: String,
#[arg(long)]
provider: Option<String>,
#[arg(long)]
model: Option<String>,
#[arg(long)]
temperature: Option<f64>,
}
fn endpoint(cli: &Cli) -> String {
if let Some(url) = &cli.rpc_url {
return url.clone();
}
std::env::var("ALPHAHUMAN_CORE_RPC_URL")
.unwrap_or_else(|_| "http://127.0.0.1:7788/rpc".to_string())
}
fn call(url: &str, method: &str, params: serde_json::Value) -> Result<serde_json::Value, String> {
let req = RpcRequest {
jsonrpc: "2.0",
id: 1,
method: method.to_string(),
params,
};
let client = reqwest::blocking::Client::new();
let resp = client
.post(url)
.json(&req)
.send()
.map_err(|e| format!("request failed: {e}"))?;
let payload: RpcResponse = resp.json().map_err(|e| format!("invalid response: {e}"))?;
if let Some(err) = payload.error {
return Err(format!(
"rpc error {}: {}{}",
err.code,
err.message,
err.data.map(|d| format!(" ({d})")).unwrap_or_default()
));
}
Ok(payload.result.unwrap_or(serde_json::Value::Null))
}
fn execute(cli: Cli) -> Result<serde_json::Value, String> {
let url = endpoint(&cli);
match cli.command {
Command::Ping => call(&url, "core.ping", serde_json::json!({})),
Command::Version => call(&url, "core.version", serde_json::json!({})),
Command::Health => call(&url, "alphahuman.health_snapshot", serde_json::json!({})),
Command::RuntimeFlags => call(&url, "alphahuman.get_runtime_flags", serde_json::json!({})),
Command::Config { command } => match command {
ConfigCommand::Get => call(&url, "alphahuman.get_config", serde_json::json!({})),
},
Command::Service { command } => match command {
ServiceCommand::Install => local_service_install(),
ServiceCommand::Start => local_service_start(),
ServiceCommand::Stop => local_service_stop(),
ServiceCommand::Status => local_service_status(),
ServiceCommand::Reinstall => {
let _ = local_service_uninstall()?;
local_service_install()
}
ServiceCommand::Uninstall => local_service_uninstall(),
},
Command::Doctor { command } => match command {
DoctorCommand::Report => call(&url, "alphahuman.doctor_report", serde_json::json!({})),
DoctorCommand::Models {
provider,
use_cache,
} => call(
&url,
"alphahuman.doctor_models",
serde_json::json!({
"provider_override": provider,
"use_cache": use_cache,
}),
),
},
Command::Integrations { command } => match command {
IntegrationsCommand::List => {
call(&url, "alphahuman.list_integrations", serde_json::json!({}))
}
IntegrationsCommand::Info { name } => call(
&url,
"alphahuman.get_integration_info",
serde_json::json!({ "name": name }),
),
},
Command::AgentChat(args) => call(
&url,
"alphahuman.agent_chat",
serde_json::json!({
"message": args.message,
"provider_override": args.provider,
"model_override": args.model,
"temperature": args.temperature,
}),
),
Command::Hardware { command } => match command {
HardwareCommand::Discover => {
call(&url, "alphahuman.hardware_discover", serde_json::json!({}))
}
HardwareCommand::Introspect { path } => call(
&url,
"alphahuman.hardware_introspect",
serde_json::json!({ "path": path }),
),
},
Command::Encrypt { plaintext } => call(
&url,
"alphahuman.encrypt_secret",
serde_json::json!({ "plaintext": plaintext }),
),
Command::Decrypt { ciphertext } => call(
&url,
"alphahuman.decrypt_secret",
serde_json::json!({ "ciphertext": ciphertext }),
),
Command::BrowserAllowAll { enabled } => call(
&url,
"alphahuman.set_browser_allow_all",
serde_json::json!({ "enabled": enabled }),
),
Command::ModelsRefresh { provider, force } => call(
&url,
"alphahuman.models_refresh",
serde_json::json!({
"provider_override": provider,
"force": force,
}),
),
Command::MigrateOpenclaw {
source_workspace,
dry_run,
} => call(
&url,
"alphahuman.migrate_openclaw",
serde_json::json!({
"source_workspace": source_workspace,
"dry_run": dry_run,
}),
),
}
}
fn load_config() -> Result<alphahuman::alphahuman::config::Config, String> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| format!("failed to build runtime: {e}"))?;
runtime
.block_on(alphahuman::alphahuman::config::Config::load_or_init())
.map_err(|e| format!("failed to load config: {e}"))
}
fn status_value(status: alphahuman::alphahuman::service::ServiceStatus) -> Result<serde_json::Value, String> {
serde_json::to_value(status).map_err(|e| format!("failed to serialize service status: {e}"))
}
fn command_response_json(
status: alphahuman::alphahuman::service::ServiceStatus,
log: &str,
) -> Result<serde_json::Value, String> {
Ok(json!({
"result": status_value(status)?,
"logs": [log],
}))
}
fn local_service_install() -> Result<serde_json::Value, String> {
let config = load_config()?;
let status = alphahuman::alphahuman::service::install(&config)
.map_err(|e| format!("service install failed: {e}"))?;
command_response_json(status, "service install completed")
}
fn local_service_start() -> Result<serde_json::Value, String> {
let config = load_config()?;
let status = alphahuman::alphahuman::service::start(&config)
.map_err(|e| format!("service start failed: {e}"))?;
command_response_json(status, "service start completed")
}
fn local_service_stop() -> Result<serde_json::Value, String> {
let config = load_config()?;
let status = alphahuman::alphahuman::service::stop(&config)
.map_err(|e| format!("service stop failed: {e}"))?;
command_response_json(status, "service stop completed")
}
fn local_service_status() -> Result<serde_json::Value, String> {
let config = load_config()?;
let status = alphahuman::alphahuman::service::status(&config)
.map_err(|e| format!("service status failed: {e}"))?;
command_response_json(status, "service status fetched")
}
fn local_service_uninstall() -> Result<serde_json::Value, String> {
let config = load_config()?;
let status = alphahuman::alphahuman::service::uninstall(&config)
.map_err(|e| format!("service uninstall failed: {e}"))?;
command_response_json(status, "service uninstall completed")
}
fn main() {
let cli = Cli::parse();
match execute(cli) {
Ok(value) => {
println!(
"{}",
serde_json::to_string_pretty(&value).unwrap_or_else(|_| "null".to_string())
);
}
Err(err) => {
eprintln!("{err}");
std::process::exit(1);
}
}
}
+7
View File
@@ -0,0 +1,7 @@
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
if let Err(err) = alphahuman::core_server::run_from_cli_args(&args) {
eprintln!("alphahuman-core failed: {err}");
std::process::exit(1);
}
}
+259 -473
View File
@@ -1,653 +1,439 @@
//! Tauri commands for the alphahuman subsystem.
//! Tauri command proxies for the standalone alphahuman core process.
use crate::alphahuman::config::Config;
use crate::alphahuman::health;
use crate::alphahuman::security::{SecretStore, SecurityPolicy};
use crate::alphahuman::{doctor, hardware, integrations, migration, onboard, service};
use crate::core_server::{
BrowserSettingsUpdate, CommandResponse, ConfigSnapshot, GatewaySettingsUpdate,
MemorySettingsUpdate, ModelSettingsUpdate, RuntimeFlags, RuntimeSettingsUpdate,
};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use tauri::Manager;
#[derive(Debug, Clone, Serialize)]
pub struct CommandResponse<T> {
pub result: T,
pub logs: Vec<String>,
const DEFAULT_CORE_RPC_URL: &str = "http://127.0.0.1:7788/rpc";
fn params_none() -> serde_json::Value {
serde_json::json!({})
}
fn command_response<T>(result: T, logs: Vec<String>) -> CommandResponse<T> {
CommandResponse { result, logs }
async fn ensure_core(app: &tauri::AppHandle) -> Result<(), String> {
let core = app
.try_state::<crate::core_process::CoreProcessHandle>()
.ok_or_else(|| "core process handle is not available".to_string())?;
let handle: crate::core_process::CoreProcessHandle = (*core).clone();
handle.ensure_running().await
}
async fn call_core<T: DeserializeOwned>(
app: &tauri::AppHandle,
method: &str,
params: serde_json::Value,
) -> Result<T, String> {
ensure_core(app).await?;
crate::core_rpc::call(method, params).await
}
async fn load_config_local() -> Result<crate::alphahuman::config::Config, String> {
let timeout_duration = std::time::Duration::from_secs(30);
match tokio::time::timeout(
timeout_duration,
crate::alphahuman::config::Config::load_or_init(),
)
.await
{
Ok(Ok(config)) => Ok(config),
Ok(Err(e)) => Err(e.to_string()),
Err(_) => Err("Config loading timed out".to_string()),
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentServerStatus {
pub running: bool,
pub url: String,
}
/// Return the current health snapshot as JSON.
#[tauri::command]
pub fn alphahuman_health_snapshot() -> CommandResponse<serde_json::Value> {
log::info!("[alphahuman:cmd] health_snapshot called");
let logs = vec!["health_snapshot requested".to_string()];
command_response(health::snapshot_json(), logs)
pub async fn alphahuman_health_snapshot(
app: tauri::AppHandle,
) -> Result<CommandResponse<serde_json::Value>, String> {
call_core(&app, "alphahuman.health_snapshot", params_none()).await
}
/// Return the default security policy info (autonomy config summary).
#[tauri::command]
pub fn alphahuman_security_policy_info() -> CommandResponse<serde_json::Value> {
log::info!("[alphahuman:cmd] security_policy_info called");
let policy = SecurityPolicy::default();
let payload = serde_json::json!({
"autonomy": policy.autonomy,
"workspace_only": policy.workspace_only,
"allowed_commands": policy.allowed_commands,
"max_actions_per_hour": policy.max_actions_per_hour,
"require_approval_for_medium_risk": policy.require_approval_for_medium_risk,
"block_high_risk_commands": policy.block_high_risk_commands,
});
let logs = vec!["security_policy_info computed".to_string()];
command_response(payload, logs)
pub async fn alphahuman_security_policy_info(
app: tauri::AppHandle,
) -> Result<CommandResponse<serde_json::Value>, String> {
call_core(&app, "alphahuman.security_policy_info", params_none()).await
}
/// Encrypt a secret using the alphahuman SecretStore.
#[tauri::command]
pub fn alphahuman_encrypt_secret(
pub async fn alphahuman_encrypt_secret(
app: tauri::AppHandle,
plaintext: String,
) -> Result<CommandResponse<String>, String> {
log::info!("[alphahuman:cmd] encrypt_secret called");
let data_dir = app
.path()
.app_data_dir()
.map_err(|e| e.to_string())?
.join("alphahuman");
let store = SecretStore::new(&data_dir, true);
store.encrypt(&plaintext).map(|ciphertext| {
command_response(ciphertext, vec!["secret encrypted".to_string()])
}).map_err(|e| {
log::error!("[alphahuman:cmd] encrypt_secret failed: {}", e);
e.to_string()
})
call_core(
&app,
"alphahuman.encrypt_secret",
serde_json::json!({ "plaintext": plaintext }),
)
.await
}
/// Decrypt a secret using the alphahuman SecretStore.
#[tauri::command]
pub fn alphahuman_decrypt_secret(
pub async fn alphahuman_decrypt_secret(
app: tauri::AppHandle,
ciphertext: String,
) -> Result<CommandResponse<String>, String> {
log::info!("[alphahuman:cmd] decrypt_secret called");
let data_dir = app
.path()
.app_data_dir()
.map_err(|e| e.to_string())?
.join("alphahuman");
let store = SecretStore::new(&data_dir, true);
store.decrypt(&ciphertext).map(|plaintext| {
command_response(plaintext, vec!["secret decrypted".to_string()])
}).map_err(|e| {
log::error!("[alphahuman:cmd] decrypt_secret failed: {}", e);
e.to_string()
})
}
async fn load_alphahuman_config() -> Result<Config, String> {
log::info!("[alphahuman:cmd] load_config called");
// Add timeout protection and proper error handling to prevent runtime panics
let timeout_duration = std::time::Duration::from_secs(30);
match tokio::time::timeout(timeout_duration, Config::load_or_init()).await {
Ok(Ok(config)) => {
log::info!("[alphahuman:cmd] config loaded successfully");
Ok(config)
}
Ok(Err(e)) => {
log::error!("[alphahuman:cmd] load config failed: {}", e);
Err(e.to_string())
}
Err(_) => {
log::error!("[alphahuman:cmd] load config timed out after 30 seconds");
Err("Config loading timed out".to_string())
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ConfigSnapshot {
pub config: serde_json::Value,
pub workspace_dir: String,
pub config_path: String,
}
fn snapshot_config(config: &Config) -> Result<ConfigSnapshot, String> {
let value = serde_json::to_value(config).map_err(|e| e.to_string())?;
Ok(ConfigSnapshot {
config: value,
workspace_dir: config.workspace_dir.display().to_string(),
config_path: config.config_path.display().to_string(),
})
}
#[derive(Debug, Clone, Deserialize)]
pub struct ModelSettingsUpdate {
pub api_key: Option<String>,
pub api_url: Option<String>,
pub default_provider: Option<String>,
pub default_model: Option<String>,
pub default_temperature: Option<f64>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct MemorySettingsUpdate {
pub backend: Option<String>,
pub auto_save: Option<bool>,
pub embedding_provider: Option<String>,
pub embedding_model: Option<String>,
pub embedding_dimensions: Option<usize>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct GatewaySettingsUpdate {
pub host: Option<String>,
pub port: Option<u16>,
pub require_pairing: Option<bool>,
pub allow_public_bind: Option<bool>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RuntimeSettingsUpdate {
pub kind: Option<String>,
pub reasoning_enabled: Option<bool>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BrowserSettingsUpdate {
pub enabled: Option<bool>,
}
#[derive(Debug, Clone, Serialize)]
pub struct RuntimeFlags {
pub browser_allow_all: bool,
pub log_prompts: bool,
}
fn env_flag_enabled(key: &str) -> bool {
matches!(
std::env::var(key).ok().as_deref(),
Some("1") | Some("true") | Some("TRUE") | Some("yes") | Some("YES")
call_core(
&app,
"alphahuman.decrypt_secret",
serde_json::json!({ "ciphertext": ciphertext }),
)
.await
}
/// Return the full Alphahuman config snapshot for UI editing.
#[tauri::command]
pub async fn alphahuman_get_config() -> Result<CommandResponse<ConfigSnapshot>, String> {
log::info!("[alphahuman:cmd] get_config called");
let config = load_alphahuman_config().await?;
let snapshot = snapshot_config(&config)?;
Ok(command_response(
snapshot,
vec![format!(
"config loaded from {}",
config.config_path.display()
)],
))
pub async fn alphahuman_get_config(
app: tauri::AppHandle,
) -> Result<CommandResponse<ConfigSnapshot>, String> {
call_core(&app, "alphahuman.get_config", params_none()).await
}
/// Update model/provider settings.
#[tauri::command]
pub async fn alphahuman_update_model_settings(
app: tauri::AppHandle,
update: ModelSettingsUpdate,
) -> Result<CommandResponse<ConfigSnapshot>, String> {
log::info!("[alphahuman:cmd] update_model_settings called");
let mut config = load_alphahuman_config().await?;
log::info!(
"[alphahuman:cmd] update_model_settings target config: {}",
config.config_path.display()
);
let ModelSettingsUpdate {
api_key,
api_url,
default_provider,
default_model,
default_temperature,
} = update;
log::info!(
"[alphahuman:cmd] update_model_settings apply: api_key={}, api_url={}, default_provider={}, default_model={}, default_temperature={}",
api_key.as_ref().map(|v| !v.trim().is_empty()).unwrap_or(false),
api_url.as_ref().map(|v| !v.trim().is_empty()).unwrap_or(false),
default_provider.as_ref().map(|v| !v.trim().is_empty()).unwrap_or(false),
default_model.as_ref().map(|v| !v.trim().is_empty()).unwrap_or(false),
default_temperature.is_some()
);
if let Some(api_key) = api_key {
config.api_key = if api_key.trim().is_empty() {
None
} else {
Some(api_key)
};
}
if let Some(api_url) = api_url {
config.api_url = if api_url.trim().is_empty() {
None
} else {
Some(api_url)
};
}
if let Some(provider) = default_provider {
config.default_provider = if provider.trim().is_empty() {
None
} else {
Some(provider)
};
}
if let Some(model) = default_model {
config.default_model = if model.trim().is_empty() {
None
} else {
Some(model)
};
}
if let Some(temp) = default_temperature {
config.default_temperature = temp;
}
config.save().await.map_err(|e| {
log::error!("[alphahuman:cmd] update_model_settings save failed: {}", e);
e.to_string()
})?;
log::info!("[alphahuman:cmd] update_model_settings saved");
let snapshot = snapshot_config(&config)?;
Ok(command_response(
snapshot,
vec![format!(
"model settings saved to {}",
config.config_path.display()
)],
))
call_core(
&app,
"alphahuman.update_model_settings",
serde_json::json!(update),
)
.await
}
/// Update memory settings.
#[tauri::command]
pub async fn alphahuman_update_memory_settings(
app: tauri::AppHandle,
update: MemorySettingsUpdate,
) -> Result<CommandResponse<ConfigSnapshot>, String> {
log::info!("[alphahuman:cmd] update_memory_settings called");
let mut config = load_alphahuman_config().await?;
if let Some(backend) = update.backend {
config.memory.backend = backend;
}
if let Some(auto_save) = update.auto_save {
config.memory.auto_save = auto_save;
}
if let Some(provider) = update.embedding_provider {
config.memory.embedding_provider = provider;
}
if let Some(model) = update.embedding_model {
config.memory.embedding_model = model;
}
if let Some(dimensions) = update.embedding_dimensions {
config.memory.embedding_dimensions = dimensions;
}
config.save().await.map_err(|e| {
log::error!("[alphahuman:cmd] update_memory_settings save failed: {}", e);
e.to_string()
})?;
log::info!("[alphahuman:cmd] update_memory_settings saved");
let snapshot = snapshot_config(&config)?;
Ok(command_response(
snapshot,
vec![format!(
"memory settings saved to {}",
config.config_path.display()
)],
))
call_core(
&app,
"alphahuman.update_memory_settings",
serde_json::json!(update),
)
.await
}
/// Update gateway settings.
#[tauri::command]
pub async fn alphahuman_update_gateway_settings(
app: tauri::AppHandle,
update: GatewaySettingsUpdate,
) -> Result<CommandResponse<ConfigSnapshot>, String> {
log::info!("[alphahuman:cmd] update_gateway_settings called");
let mut config = load_alphahuman_config().await?;
if let Some(host) = update.host {
config.gateway.host = host;
}
if let Some(port) = update.port {
config.gateway.port = port;
}
if let Some(require_pairing) = update.require_pairing {
config.gateway.require_pairing = require_pairing;
}
if let Some(allow_public_bind) = update.allow_public_bind {
config.gateway.allow_public_bind = allow_public_bind;
}
config.save().await.map_err(|e| {
log::error!("[alphahuman:cmd] update_gateway_settings save failed: {}", e);
e.to_string()
})?;
log::info!("[alphahuman:cmd] update_gateway_settings saved");
let snapshot = snapshot_config(&config)?;
Ok(command_response(
snapshot,
vec![format!(
"gateway settings saved to {}",
config.config_path.display()
)],
))
call_core(
&app,
"alphahuman.update_gateway_settings",
serde_json::json!(update),
)
.await
}
/// Update tunnel settings (full tunnel config).
#[tauri::command]
pub async fn alphahuman_update_tunnel_settings(
app: tauri::AppHandle,
tunnel: crate::alphahuman::config::TunnelConfig,
) -> Result<CommandResponse<ConfigSnapshot>, String> {
log::info!("[alphahuman:cmd] update_tunnel_settings called");
let mut config = load_alphahuman_config().await?;
config.tunnel = tunnel;
config.save().await.map_err(|e| {
log::error!("[alphahuman:cmd] update_tunnel_settings save failed: {}", e);
e.to_string()
})?;
log::info!("[alphahuman:cmd] update_tunnel_settings saved");
let snapshot = snapshot_config(&config)?;
Ok(command_response(
snapshot,
vec![format!(
"tunnel settings saved to {}",
config.config_path.display()
)],
))
call_core(
&app,
"alphahuman.update_tunnel_settings",
serde_json::json!(tunnel),
)
.await
}
/// Update runtime settings (skill execution backend).
#[tauri::command]
pub async fn alphahuman_update_runtime_settings(
app: tauri::AppHandle,
update: RuntimeSettingsUpdate,
) -> Result<CommandResponse<ConfigSnapshot>, String> {
log::info!("[alphahuman:cmd] update_runtime_settings called");
let mut config = load_alphahuman_config().await?;
if let Some(kind) = update.kind {
config.runtime.kind = kind;
}
if let Some(reasoning_enabled) = update.reasoning_enabled {
config.runtime.reasoning_enabled = Some(reasoning_enabled);
}
config.save().await.map_err(|e| {
log::error!("[alphahuman:cmd] update_runtime_settings save failed: {}", e);
e.to_string()
})?;
log::info!("[alphahuman:cmd] update_runtime_settings saved");
let snapshot = snapshot_config(&config)?;
Ok(command_response(
snapshot,
vec![format!(
"runtime settings saved to {}",
config.config_path.display()
)],
))
call_core(
&app,
"alphahuman.update_runtime_settings",
serde_json::json!(update),
)
.await
}
/// Update browser settings (Chrome/Chromium tool).
#[tauri::command]
pub async fn alphahuman_update_browser_settings(
app: tauri::AppHandle,
update: BrowserSettingsUpdate,
) -> Result<CommandResponse<ConfigSnapshot>, String> {
log::info!("[alphahuman:cmd] update_browser_settings called");
log::debug!("[alphahuman:cmd] update_browser_settings requested");
let mut config = load_alphahuman_config().await?;
if let Some(enabled) = update.enabled {
config.browser.enabled = enabled;
}
config.save().await.map_err(|e| {
log::error!("[alphahuman:cmd] update_browser_settings save failed: {}", e);
e.to_string()
})?;
log::info!("[alphahuman:cmd] update_browser_settings saved");
let snapshot = snapshot_config(&config)?;
Ok(command_response(
snapshot,
vec![format!(
"browser settings saved to {}",
config.config_path.display()
)],
))
call_core(
&app,
"alphahuman.update_browser_settings",
serde_json::json!(update),
)
.await
}
/// Read runtime flags that are controlled via environment variables.
#[tauri::command]
pub fn alphahuman_get_runtime_flags() -> CommandResponse<RuntimeFlags> {
log::info!("[alphahuman:cmd] get_runtime_flags called");
let flags = RuntimeFlags {
browser_allow_all: env_flag_enabled("ALPHAHUMAN_BROWSER_ALLOW_ALL"),
log_prompts: env_flag_enabled("ALPHAHUMAN_LOG_PROMPTS"),
};
command_response(flags, vec!["runtime flags read".to_string()])
pub async fn alphahuman_get_runtime_flags(
app: tauri::AppHandle,
) -> Result<CommandResponse<RuntimeFlags>, String> {
call_core(&app, "alphahuman.get_runtime_flags", params_none()).await
}
/// Set browser allow-all flag for the current process.
#[tauri::command]
pub fn alphahuman_set_browser_allow_all(
pub async fn alphahuman_set_browser_allow_all(
app: tauri::AppHandle,
enabled: bool,
) -> CommandResponse<RuntimeFlags> {
log::info!("[alphahuman:cmd] set_browser_allow_all called");
if enabled {
std::env::set_var("ALPHAHUMAN_BROWSER_ALLOW_ALL", "1");
} else {
std::env::remove_var("ALPHAHUMAN_BROWSER_ALLOW_ALL");
}
let flags = RuntimeFlags {
browser_allow_all: env_flag_enabled("ALPHAHUMAN_BROWSER_ALLOW_ALL"),
log_prompts: env_flag_enabled("ALPHAHUMAN_LOG_PROMPTS"),
};
command_response(flags, vec!["browser allow-all flag updated".to_string()])
) -> Result<CommandResponse<RuntimeFlags>, String> {
call_core(
&app,
"alphahuman.set_browser_allow_all",
serde_json::json!({ "enabled": enabled }),
)
.await
}
/// Send a single message to the Alphahuman agent and return the response text.
#[tauri::command]
pub async fn alphahuman_agent_chat(
app: tauri::AppHandle,
message: String,
provider_override: Option<String>,
model_override: Option<String>,
temperature: Option<f64>,
) -> Result<CommandResponse<String>, String> {
log::info!("[alphahuman:cmd] agent_chat called");
let mut config = load_alphahuman_config().await?;
if let Some(provider) = provider_override {
config.default_provider = Some(provider);
}
if let Some(model) = model_override {
config.default_model = Some(model);
}
if let Some(temp) = temperature {
config.default_temperature = temp;
}
let mut agent = crate::alphahuman::agent::Agent::from_config(&config)
.map_err(|e| {
log::error!("[alphahuman:cmd] agent_chat build failed: {}", e);
e.to_string()
})?;
let response = agent.run_single(&message).await.map_err(|e| {
log::error!("[alphahuman:cmd] agent_chat run failed: {}", e);
e.to_string()
})?;
Ok(command_response(
response,
vec!["agent chat completed".to_string()],
))
call_core(
&app,
"alphahuman.agent_chat",
serde_json::json!({
"message": message,
"provider_override": provider_override,
"model_override": model_override,
"temperature": temperature,
}),
)
.await
}
/// Run Alphahuman doctor checks and return a structured report.
#[tauri::command]
pub async fn alphahuman_doctor_report() -> Result<CommandResponse<doctor::DoctorReport>, String> {
log::info!("[alphahuman:cmd] doctor_report called");
let config = load_alphahuman_config().await?;
doctor::run(&config).map(|report| {
command_response(report, vec!["doctor report generated".to_string()])
}).map_err(|e| e.to_string())
pub async fn alphahuman_doctor_report(
app: tauri::AppHandle,
) -> Result<CommandResponse<doctor::DoctorReport>, String> {
call_core(&app, "alphahuman.doctor_report", params_none()).await
}
/// Run model catalog probes for providers.
#[tauri::command]
pub async fn alphahuman_doctor_models(
app: tauri::AppHandle,
provider_override: Option<String>,
use_cache: Option<bool>,
) -> Result<CommandResponse<doctor::ModelProbeReport>, String> {
log::info!("[alphahuman:cmd] doctor_models called");
let config = load_alphahuman_config().await?;
let use_cache = use_cache.unwrap_or(true);
doctor::run_models(&config, provider_override.as_deref(), use_cache)
.map(|report| command_response(report, vec!["model probes completed".to_string()]))
.map_err(|e| e.to_string())
call_core(
&app,
"alphahuman.doctor_models",
serde_json::json!({
"provider_override": provider_override,
"use_cache": use_cache,
}),
)
.await
}
/// List integrations with status for the current config.
#[tauri::command]
pub async fn alphahuman_list_integrations(
app: tauri::AppHandle,
) -> Result<CommandResponse<Vec<integrations::IntegrationInfo>>, String> {
log::info!("[alphahuman:cmd] list_integrations called");
let config = load_alphahuman_config().await?;
Ok(command_response(
integrations::list_integrations(&config),
vec!["integrations listed".to_string()],
))
call_core(&app, "alphahuman.list_integrations", params_none()).await
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct IntegrationInfoParams {
name: String,
}
/// Get details for a single integration.
#[tauri::command]
pub async fn alphahuman_get_integration_info(
app: tauri::AppHandle,
name: String,
) -> Result<CommandResponse<integrations::IntegrationInfo>, String> {
log::info!("[alphahuman:cmd] get_integration_info called");
let config = load_alphahuman_config().await?;
integrations::get_integration_info(&config, &name)
.map(|info| command_response(info, vec![format!("integration loaded: {name}")]))
.map_err(|e| {
log::error!("[alphahuman:cmd] get_integration_info failed: {}", e);
e.to_string()
})
let params = IntegrationInfoParams { name };
call_core(
&app,
"alphahuman.get_integration_info",
serde_json::json!(params),
)
.await
}
/// Refresh the model catalog for a provider (or default provider).
#[tauri::command]
pub async fn alphahuman_models_refresh(
app: tauri::AppHandle,
provider_override: Option<String>,
force: Option<bool>,
) -> Result<CommandResponse<onboard::ModelRefreshResult>, String> {
log::info!("[alphahuman:cmd] models_refresh called");
let config = load_alphahuman_config().await?;
let force = force.unwrap_or(false);
onboard::run_models_refresh(&config, provider_override.as_deref(), force)
.map(|result| command_response(result, vec!["model refresh completed".to_string()]))
.map_err(|e| {
log::error!("[alphahuman:cmd] models_refresh failed: {}", e);
e.to_string()
})
call_core(
&app,
"alphahuman.models_refresh",
serde_json::json!({
"provider_override": provider_override,
"force": force,
}),
)
.await
}
/// Migrate OpenClaw memory into the current Alphahuman workspace.
#[tauri::command]
pub async fn alphahuman_migrate_openclaw(
app: tauri::AppHandle,
source_workspace: Option<String>,
dry_run: Option<bool>,
) -> Result<CommandResponse<migration::MigrationReport>, String> {
log::info!("[alphahuman:cmd] migrate_openclaw called");
let config = load_alphahuman_config().await?;
let source = source_workspace.map(std::path::PathBuf::from);
let dry_run = dry_run.unwrap_or(true);
migration::migrate_openclaw_memory(&config, source, dry_run)
.await
.map(|report| command_response(report, vec!["migration completed".to_string()]))
.map_err(|e| {
log::error!("[alphahuman:cmd] migrate_openclaw failed: {}", e);
e.to_string()
})
call_core(
&app,
"alphahuman.migrate_openclaw",
serde_json::json!({
"source_workspace": source_workspace,
"dry_run": dry_run,
}),
)
.await
}
/// Discover connected hardware devices (feature-gated).
#[tauri::command]
pub fn alphahuman_hardware_discover() -> CommandResponse<Vec<hardware::DiscoveredDevice>> {
log::info!("[alphahuman:cmd] hardware_discover called");
command_response(
hardware::discover_hardware(),
vec!["hardware discovery complete".to_string()],
)
pub async fn alphahuman_hardware_discover(
app: tauri::AppHandle,
) -> Result<CommandResponse<Vec<hardware::DiscoveredDevice>>, String> {
call_core(&app, "alphahuman.hardware_discover", params_none()).await
}
/// Introspect a device path (feature-gated).
#[tauri::command]
pub fn alphahuman_hardware_introspect(
pub async fn alphahuman_hardware_introspect(
app: tauri::AppHandle,
path: String,
) -> Result<CommandResponse<hardware::HardwareIntrospect>, String> {
log::info!("[alphahuman:cmd] hardware_introspect called");
hardware::introspect_device(&path)
.map(|info| command_response(info, vec![format!("introspected {path}")]))
.map_err(|e| {
log::error!("[alphahuman:cmd] hardware_introspect failed: {}", e);
e.to_string()
call_core(
&app,
"alphahuman.hardware_introspect",
serde_json::json!({ "path": path }),
)
.await
}
/// Return whether the local core agent server is reachable.
#[tauri::command]
pub async fn alphahuman_agent_server_status() -> Result<CommandResponse<AgentServerStatus>, String> {
let url = std::env::var("ALPHAHUMAN_CORE_RPC_URL").unwrap_or_else(|_| DEFAULT_CORE_RPC_URL.to_string());
let running = crate::core_rpc::ping().await;
Ok(CommandResponse {
result: AgentServerStatus { running, url },
logs: vec!["agent server status checked".to_string()],
})
}
/// Install the Alphahuman daemon service.
#[tauri::command]
pub async fn alphahuman_service_install(
_app: tauri::AppHandle,
) -> Result<CommandResponse<service::ServiceStatus>, String> {
log::info!("[alphahuman:cmd] service_install called");
let config = load_alphahuman_config().await?;
let config = load_config_local().await?;
service::install(&config)
.map(|status| command_response(status, vec!["service install completed".to_string()]))
.map_err(|e| {
log::error!("[alphahuman:cmd] service_install failed: {}", e);
e.to_string()
.map(|status| CommandResponse {
result: status,
logs: vec!["service install completed".to_string()],
})
.map_err(|e| e.to_string())
}
/// Start the Alphahuman daemon service.
#[tauri::command]
pub async fn alphahuman_service_start() -> Result<CommandResponse<service::ServiceStatus>, String> {
log::info!("[alphahuman:cmd] service_start called");
let config = load_alphahuman_config().await?;
pub async fn alphahuman_service_start(
_app: tauri::AppHandle,
) -> Result<CommandResponse<service::ServiceStatus>, String> {
let config = load_config_local().await?;
service::start(&config)
.map(|status| {
log::info!("[alphahuman:cmd] External daemon service started - health events should be emitted from service process");
command_response(status, vec!["service start completed".to_string()])
})
.map_err(|e| {
log::error!("[alphahuman:cmd] service_start failed: {}", e);
e.to_string()
.map(|status| CommandResponse {
result: status,
logs: vec!["service start completed".to_string()],
})
.map_err(|e| e.to_string())
}
/// Stop the Alphahuman daemon service.
#[tauri::command]
pub async fn alphahuman_service_stop() -> Result<CommandResponse<service::ServiceStatus>, String> {
log::info!("[alphahuman:cmd] service_stop called");
let config = load_alphahuman_config().await?;
service::stop(&config)
.map(|status| command_response(status, vec!["service stop completed".to_string()]))
.map_err(|e| {
log::error!("[alphahuman:cmd] service_stop failed: {}", e);
e.to_string()
})
pub async fn alphahuman_service_stop(
app: tauri::AppHandle,
) -> Result<CommandResponse<service::ServiceStatus>, String> {
let config = load_config_local().await?;
let status = service::stop(&config)
.map_err(|e| e.to_string())?;
// Also stop any locally managed core process in this Tauri app.
if let Some(core) = app.try_state::<crate::core_process::CoreProcessHandle>() {
let core_handle: crate::core_process::CoreProcessHandle = (*core).clone();
core_handle.shutdown().await;
}
Ok(CommandResponse {
result: status,
logs: vec!["service stop completed".to_string()],
})
}
/// Get the Alphahuman daemon service status.
#[tauri::command]
pub async fn alphahuman_service_status(
_app: tauri::AppHandle,
) -> Result<CommandResponse<service::ServiceStatus>, String> {
log::info!("[alphahuman:cmd] service_status called");
let config = load_alphahuman_config().await?;
let config = load_config_local().await?;
service::status(&config)
.map(|status| command_response(status, vec!["service status fetched".to_string()]))
.map_err(|e| {
log::error!("[alphahuman:cmd] service_status failed: {}", e);
e.to_string()
.map(|status| CommandResponse {
result: status,
logs: vec!["service status fetched".to_string()],
})
.map_err(|e| e.to_string())
}
/// Uninstall the Alphahuman daemon service.
#[tauri::command]
pub async fn alphahuman_service_uninstall(
app: tauri::AppHandle,
) -> Result<CommandResponse<service::ServiceStatus>, String> {
log::info!("[alphahuman:cmd] service_uninstall called");
let config = load_alphahuman_config().await?;
service::uninstall(&config)
.map(|status| command_response(status, vec!["service uninstall completed".to_string()]))
.map_err(|e| {
log::error!("[alphahuman:cmd] service_uninstall failed: {}", e);
e.to_string()
})
let config = load_config_local().await?;
let status = service::uninstall(&config)
.map_err(|e| e.to_string())?;
if let Some(core) = app.try_state::<crate::core_process::CoreProcessHandle>() {
let core_handle: crate::core_process::CoreProcessHandle = (*core).clone();
core_handle.shutdown().await;
}
Ok(CommandResponse {
result: status,
logs: vec!["service uninstall completed".to_string()],
})
}
+88
View File
@@ -0,0 +1,88 @@
use std::sync::Arc;
use tokio::process::{Child, Command};
use tokio::sync::Mutex;
#[derive(Clone)]
pub struct CoreProcessHandle {
child: Arc<Mutex<Option<Child>>>,
port: u16,
}
impl CoreProcessHandle {
pub fn new(port: u16) -> Self {
Self {
child: Arc::new(Mutex::new(None)),
port,
}
}
pub fn rpc_url(&self) -> String {
format!("http://127.0.0.1:{}/rpc", self.port)
}
pub async fn ensure_running(&self) -> Result<(), String> {
if crate::core_rpc::ping().await {
log::info!("[core] found existing core rpc endpoint at {}", self.rpc_url());
return Ok(());
}
let mut guard = self.child.lock().await;
if guard.is_none() {
let exe = std::env::current_exe()
.map_err(|e| format!("failed to resolve current executable: {e}"))?;
let mut cmd = Command::new(exe);
cmd.arg("core")
.arg("serve")
.arg("--port")
.arg(self.port.to_string());
log::info!("[core] spawning core process: {:?} core serve --port {}", cmd.as_std().get_program(), self.port);
let child = cmd
.spawn()
.map_err(|e| format!("failed to spawn core process: {e}"))?;
*guard = Some(child);
}
drop(guard);
for _ in 0..40 {
if crate::core_rpc::ping().await {
log::info!("[core] core rpc became ready at {}", self.rpc_url());
return Ok(());
}
let mut guard = self.child.lock().await;
if let Some(child) = guard.as_mut() {
match child.try_wait() {
Ok(Some(status)) => {
return Err(format!("core process exited before ready: {status}"));
}
Ok(None) => {}
Err(e) => return Err(format!("failed checking core process status: {e}")),
}
}
drop(guard);
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
Err("core process did not become ready".to_string())
}
pub async fn shutdown(&self) {
let mut guard = self.child.lock().await;
if let Some(child) = guard.as_mut() {
let _ = child.kill().await;
}
*guard = None;
}
}
pub fn default_core_port() -> u16 {
std::env::var("ALPHAHUMAN_CORE_PORT")
.ok()
.and_then(|v| v.parse::<u16>().ok())
.unwrap_or(7788)
}
+86
View File
@@ -0,0 +1,86 @@
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
const DEFAULT_CORE_RPC_URL: &str = "http://127.0.0.1:7788/rpc";
#[derive(Debug, Serialize)]
struct RpcRequest<'a> {
jsonrpc: &'static str,
id: u64,
method: &'a str,
params: serde_json::Value,
}
#[derive(Debug, Deserialize)]
struct RpcResponse<T> {
#[allow(dead_code)]
jsonrpc: String,
#[allow(dead_code)]
id: serde_json::Value,
result: Option<T>,
error: Option<RpcError>,
}
#[derive(Debug, Deserialize)]
struct RpcError {
#[allow(dead_code)]
code: i64,
message: String,
data: Option<serde_json::Value>,
}
fn rpc_url() -> String {
std::env::var("ALPHAHUMAN_CORE_RPC_URL").unwrap_or_else(|_| DEFAULT_CORE_RPC_URL.to_string())
}
pub async fn call<T: DeserializeOwned>(
method: &str,
params: serde_json::Value,
) -> Result<T, String> {
let client = reqwest::Client::new();
let req = RpcRequest {
jsonrpc: "2.0",
id: 1,
method,
params,
};
let resp = client
.post(rpc_url())
.json(&req)
.send()
.await
.map_err(|e| format!("core rpc request failed: {e}"))?;
let status = resp.status();
if !status.is_success() {
let text = resp.text().await.unwrap_or_default();
return Err(format!("core rpc http error {status}: {text}"));
}
let payload: RpcResponse<T> = resp
.json()
.await
.map_err(|e| format!("core rpc decode failed: {e}"))?;
if let Some(err) = payload.error {
let data = err.data.map(|d| format!(" ({d})")).unwrap_or_default();
return Err(format!("{}{}", err.message, data));
}
payload
.result
.ok_or_else(|| "core rpc missing result".to_string())
}
pub async fn ping() -> bool {
#[derive(Deserialize)]
struct Pong {
ok: bool,
}
match call::<Pong>("core.ping", serde_json::json!({})).await {
Ok(p) => p.ok,
Err(_) => false,
}
}
+728
View File
@@ -0,0 +1,728 @@
use anyhow::Result;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post};
use axum::{Json, Router};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::alphahuman::config::Config;
use crate::alphahuman::health;
use crate::alphahuman::security::{SecretStore, SecurityPolicy};
use crate::alphahuman::{doctor, hardware, integrations, migration, onboard, service};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandResponse<T> {
pub result: T,
pub logs: Vec<String>,
}
fn command_response<T>(result: T, logs: Vec<String>) -> CommandResponse<T> {
CommandResponse { result, logs }
}
#[derive(Debug, Deserialize)]
struct RpcRequest {
#[allow(dead_code)]
jsonrpc: String,
id: serde_json::Value,
method: String,
#[serde(default)]
params: serde_json::Value,
}
#[derive(Debug, Serialize)]
struct RpcSuccess {
jsonrpc: &'static str,
id: serde_json::Value,
result: serde_json::Value,
}
#[derive(Debug, Serialize)]
struct RpcFailure {
jsonrpc: &'static str,
id: serde_json::Value,
error: RpcError,
}
#[derive(Debug, Serialize)]
struct RpcError {
code: i64,
message: String,
data: Option<serde_json::Value>,
}
#[derive(Clone)]
struct AppState {
core_version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigSnapshot {
pub config: serde_json::Value,
pub workspace_dir: String,
pub config_path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelSettingsUpdate {
pub api_key: Option<String>,
pub api_url: Option<String>,
pub default_provider: Option<String>,
pub default_model: Option<String>,
pub default_temperature: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemorySettingsUpdate {
pub backend: Option<String>,
pub auto_save: Option<bool>,
pub embedding_provider: Option<String>,
pub embedding_model: Option<String>,
pub embedding_dimensions: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GatewaySettingsUpdate {
pub host: Option<String>,
pub port: Option<u16>,
pub require_pairing: Option<bool>,
pub allow_public_bind: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuntimeSettingsUpdate {
pub kind: Option<String>,
pub reasoning_enabled: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrowserSettingsUpdate {
pub enabled: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuntimeFlags {
pub browser_allow_all: bool,
pub log_prompts: bool,
}
#[derive(Debug, Deserialize)]
struct AgentChatParams {
message: String,
provider_override: Option<String>,
model_override: Option<String>,
temperature: Option<f64>,
}
#[derive(Debug, Deserialize)]
struct DoctorModelsParams {
provider_override: Option<String>,
use_cache: Option<bool>,
}
#[derive(Debug, Deserialize)]
struct IntegrationInfoParams {
name: String,
}
#[derive(Debug, Deserialize)]
struct ModelsRefreshParams {
provider_override: Option<String>,
force: Option<bool>,
}
#[derive(Debug, Deserialize)]
struct MigrateOpenClawParams {
source_workspace: Option<String>,
dry_run: Option<bool>,
}
#[derive(Debug, Deserialize)]
struct HardwareIntrospectParams {
path: String,
}
#[derive(Debug, Deserialize)]
struct EncryptSecretParams {
plaintext: String,
}
#[derive(Debug, Deserialize)]
struct DecryptSecretParams {
ciphertext: String,
}
#[derive(Debug, Deserialize)]
struct SetBrowserAllowAllParams {
enabled: bool,
}
async fn load_alphahuman_config() -> Result<Config, String> {
let timeout_duration = std::time::Duration::from_secs(30);
match tokio::time::timeout(timeout_duration, Config::load_or_init()).await {
Ok(Ok(config)) => Ok(config),
Ok(Err(e)) => Err(e.to_string()),
Err(_) => Err("Config loading timed out".to_string()),
}
}
fn snapshot_config(config: &Config) -> Result<ConfigSnapshot, String> {
let value = serde_json::to_value(config).map_err(|e| e.to_string())?;
Ok(ConfigSnapshot {
config: value,
workspace_dir: config.workspace_dir.display().to_string(),
config_path: config.config_path.display().to_string(),
})
}
fn env_flag_enabled(key: &str) -> bool {
matches!(
std::env::var(key).ok().as_deref(),
Some("1") | Some("true") | Some("TRUE") | Some("yes") | Some("YES")
)
}
fn secret_store_for_config(config: &Config) -> SecretStore {
let data_dir = config
.config_path
.parent()
.map_or_else(|| std::path::PathBuf::from("."), std::path::PathBuf::from);
SecretStore::new(&data_dir, true)
}
fn parse_params<T: DeserializeOwned>(params: serde_json::Value) -> Result<T, String> {
serde_json::from_value(params).map_err(|e| format!("invalid params: {e}"))
}
fn rpc_error_response(id: serde_json::Value, code: i64, message: String) -> Response {
(
StatusCode::OK,
Json(RpcFailure {
jsonrpc: "2.0",
id,
error: RpcError {
code,
message,
data: None,
},
}),
)
.into_response()
}
fn to_rpc_success(id: serde_json::Value, value: serde_json::Value) -> Response {
(
StatusCode::OK,
Json(RpcSuccess {
jsonrpc: "2.0",
id,
result: value,
}),
)
.into_response()
}
fn to_json_value<T: Serialize>(value: T) -> Result<serde_json::Value, String> {
serde_json::to_value(value).map_err(|e| e.to_string())
}
async fn rpc_handler(State(state): State<AppState>, Json(req): Json<RpcRequest>) -> Response {
let id = req.id.clone();
let result = dispatch(state, req.method.as_str(), req.params).await;
match result {
Ok(value) => to_rpc_success(id, value),
Err(message) => rpc_error_response(id, -32000, message),
}
}
async fn dispatch(
state: AppState,
method: &str,
params: serde_json::Value,
) -> Result<serde_json::Value, String> {
match method {
"core.ping" => to_json_value(json!({ "ok": true })),
"core.version" => to_json_value(json!({ "version": state.core_version })),
"alphahuman.health_snapshot" => to_json_value(command_response(
health::snapshot_json(),
vec!["health_snapshot requested".to_string()],
)),
"alphahuman.security_policy_info" => {
let policy = SecurityPolicy::default();
let payload = json!({
"autonomy": policy.autonomy,
"workspace_only": policy.workspace_only,
"allowed_commands": policy.allowed_commands,
"max_actions_per_hour": policy.max_actions_per_hour,
"require_approval_for_medium_risk": policy.require_approval_for_medium_risk,
"block_high_risk_commands": policy.block_high_risk_commands,
});
to_json_value(command_response(
payload,
vec!["security_policy_info computed".to_string()],
))
}
"alphahuman.get_config" => {
let config = load_alphahuman_config().await?;
let snapshot = snapshot_config(&config)?;
to_json_value(command_response(
snapshot,
vec![format!(
"config loaded from {}",
config.config_path.display()
)],
))
}
"alphahuman.update_model_settings" => {
let update: ModelSettingsUpdate = parse_params(params)?;
let mut config = load_alphahuman_config().await?;
if let Some(api_key) = update.api_key {
config.api_key = if api_key.trim().is_empty() {
None
} else {
Some(api_key)
};
}
if let Some(api_url) = update.api_url {
config.api_url = if api_url.trim().is_empty() {
None
} else {
Some(api_url)
};
}
if let Some(provider) = update.default_provider {
config.default_provider = if provider.trim().is_empty() {
None
} else {
Some(provider)
};
}
if let Some(model) = update.default_model {
config.default_model = if model.trim().is_empty() {
None
} else {
Some(model)
};
}
if let Some(temp) = update.default_temperature {
config.default_temperature = temp;
}
config.save().await.map_err(|e| e.to_string())?;
let snapshot = snapshot_config(&config)?;
to_json_value(command_response(
snapshot,
vec![format!(
"model settings saved to {}",
config.config_path.display()
)],
))
}
"alphahuman.update_memory_settings" => {
let update: MemorySettingsUpdate = parse_params(params)?;
let mut config = load_alphahuman_config().await?;
if let Some(backend) = update.backend {
config.memory.backend = backend;
}
if let Some(auto_save) = update.auto_save {
config.memory.auto_save = auto_save;
}
if let Some(provider) = update.embedding_provider {
config.memory.embedding_provider = provider;
}
if let Some(model) = update.embedding_model {
config.memory.embedding_model = model;
}
if let Some(dimensions) = update.embedding_dimensions {
config.memory.embedding_dimensions = dimensions;
}
config.save().await.map_err(|e| e.to_string())?;
let snapshot = snapshot_config(&config)?;
to_json_value(command_response(
snapshot,
vec![format!(
"memory settings saved to {}",
config.config_path.display()
)],
))
}
"alphahuman.update_gateway_settings" => {
let update: GatewaySettingsUpdate = parse_params(params)?;
let mut config = load_alphahuman_config().await?;
if let Some(host) = update.host {
config.gateway.host = host;
}
if let Some(port) = update.port {
config.gateway.port = port;
}
if let Some(require_pairing) = update.require_pairing {
config.gateway.require_pairing = require_pairing;
}
if let Some(allow_public_bind) = update.allow_public_bind {
config.gateway.allow_public_bind = allow_public_bind;
}
config.save().await.map_err(|e| e.to_string())?;
let snapshot = snapshot_config(&config)?;
to_json_value(command_response(
snapshot,
vec![format!(
"gateway settings saved to {}",
config.config_path.display()
)],
))
}
"alphahuman.update_tunnel_settings" => {
let tunnel: crate::alphahuman::config::TunnelConfig = parse_params(params)?;
let mut config = load_alphahuman_config().await?;
config.tunnel = tunnel;
config.save().await.map_err(|e| e.to_string())?;
let snapshot = snapshot_config(&config)?;
to_json_value(command_response(
snapshot,
vec![format!(
"tunnel settings saved to {}",
config.config_path.display()
)],
))
}
"alphahuman.update_runtime_settings" => {
let update: RuntimeSettingsUpdate = parse_params(params)?;
let mut config = load_alphahuman_config().await?;
if let Some(kind) = update.kind {
config.runtime.kind = kind;
}
if let Some(reasoning_enabled) = update.reasoning_enabled {
config.runtime.reasoning_enabled = Some(reasoning_enabled);
}
config.save().await.map_err(|e| e.to_string())?;
let snapshot = snapshot_config(&config)?;
to_json_value(command_response(
snapshot,
vec![format!(
"runtime settings saved to {}",
config.config_path.display()
)],
))
}
"alphahuman.update_browser_settings" => {
let update: BrowserSettingsUpdate = parse_params(params)?;
let mut config = load_alphahuman_config().await?;
if let Some(enabled) = update.enabled {
config.browser.enabled = enabled;
}
config.save().await.map_err(|e| e.to_string())?;
let snapshot = snapshot_config(&config)?;
to_json_value(command_response(
snapshot,
vec![format!(
"browser settings saved to {}",
config.config_path.display()
)],
))
}
"alphahuman.get_runtime_flags" => {
let flags = RuntimeFlags {
browser_allow_all: env_flag_enabled("ALPHAHUMAN_BROWSER_ALLOW_ALL"),
log_prompts: env_flag_enabled("ALPHAHUMAN_LOG_PROMPTS"),
};
to_json_value(command_response(
flags,
vec!["runtime flags read".to_string()],
))
}
"alphahuman.set_browser_allow_all" => {
let p: SetBrowserAllowAllParams = parse_params(params)?;
if p.enabled {
std::env::set_var("ALPHAHUMAN_BROWSER_ALLOW_ALL", "1");
} else {
std::env::remove_var("ALPHAHUMAN_BROWSER_ALLOW_ALL");
}
let flags = RuntimeFlags {
browser_allow_all: env_flag_enabled("ALPHAHUMAN_BROWSER_ALLOW_ALL"),
log_prompts: env_flag_enabled("ALPHAHUMAN_LOG_PROMPTS"),
};
to_json_value(command_response(
flags,
vec!["browser allow-all flag updated".to_string()],
))
}
"alphahuman.agent_chat" => {
let p: AgentChatParams = parse_params(params)?;
let mut config = load_alphahuman_config().await?;
if let Some(provider) = p.provider_override {
config.default_provider = Some(provider);
}
if let Some(model) = p.model_override {
config.default_model = Some(model);
}
if let Some(temp) = p.temperature {
config.default_temperature = temp;
}
let mut agent =
crate::alphahuman::agent::Agent::from_config(&config).map_err(|e| e.to_string())?;
let response = agent
.run_single(&p.message)
.await
.map_err(|e| e.to_string())?;
to_json_value(command_response(
response,
vec!["agent chat completed".to_string()],
))
}
"alphahuman.encrypt_secret" => {
let p: EncryptSecretParams = parse_params(params)?;
let config = load_alphahuman_config().await?;
let store = secret_store_for_config(&config);
let ciphertext = store.encrypt(&p.plaintext).map_err(|e| e.to_string())?;
to_json_value(command_response(
ciphertext,
vec!["secret encrypted".to_string()],
))
}
"alphahuman.decrypt_secret" => {
let p: DecryptSecretParams = parse_params(params)?;
let config = load_alphahuman_config().await?;
let store = secret_store_for_config(&config);
let plaintext = store.decrypt(&p.ciphertext).map_err(|e| e.to_string())?;
to_json_value(command_response(
plaintext,
vec!["secret decrypted".to_string()],
))
}
"alphahuman.doctor_report" => {
let config = load_alphahuman_config().await?;
let report = doctor::run(&config).map_err(|e| e.to_string())?;
to_json_value(command_response(
report,
vec!["doctor report generated".to_string()],
))
}
"alphahuman.doctor_models" => {
let p: DoctorModelsParams = parse_params(params)?;
let config = load_alphahuman_config().await?;
let use_cache = p.use_cache.unwrap_or(true);
let report = doctor::run_models(&config, p.provider_override.as_deref(), use_cache)
.map_err(|e| e.to_string())?;
to_json_value(command_response(
report,
vec!["model probes completed".to_string()],
))
}
"alphahuman.list_integrations" => {
let config = load_alphahuman_config().await?;
to_json_value(command_response(
integrations::list_integrations(&config),
vec!["integrations listed".to_string()],
))
}
"alphahuman.get_integration_info" => {
let p: IntegrationInfoParams = parse_params(params)?;
let config = load_alphahuman_config().await?;
let info =
integrations::get_integration_info(&config, &p.name).map_err(|e| e.to_string())?;
to_json_value(command_response(
info,
vec![format!("integration loaded: {}", p.name)],
))
}
"alphahuman.models_refresh" => {
let p: ModelsRefreshParams = parse_params(params)?;
let config = load_alphahuman_config().await?;
let result = onboard::run_models_refresh(
&config,
p.provider_override.as_deref(),
p.force.unwrap_or(false),
)
.map_err(|e| e.to_string())?;
to_json_value(command_response(
result,
vec!["model refresh completed".to_string()],
))
}
"alphahuman.migrate_openclaw" => {
let p: MigrateOpenClawParams = parse_params(params)?;
let config = load_alphahuman_config().await?;
let source = p.source_workspace.map(std::path::PathBuf::from);
let report =
migration::migrate_openclaw_memory(&config, source, p.dry_run.unwrap_or(true))
.await
.map_err(|e| e.to_string())?;
to_json_value(command_response(
report,
vec!["migration completed".to_string()],
))
}
"alphahuman.hardware_discover" => to_json_value(command_response(
hardware::discover_hardware(),
vec!["hardware discovery complete".to_string()],
)),
"alphahuman.hardware_introspect" => {
let p: HardwareIntrospectParams = parse_params(params)?;
let info = hardware::introspect_device(&p.path).map_err(|e| e.to_string())?;
to_json_value(command_response(
info,
vec![format!("introspected {}", p.path)],
))
}
"alphahuman.service_install" => {
let config = load_alphahuman_config().await?;
let status = service::install(&config).map_err(|e| e.to_string())?;
to_json_value(command_response(
status,
vec!["service install completed".to_string()],
))
}
"alphahuman.service_start" => {
let config = load_alphahuman_config().await?;
let status = service::start(&config).map_err(|e| e.to_string())?;
to_json_value(command_response(
status,
vec!["service start completed".to_string()],
))
}
"alphahuman.service_stop" => {
let config = load_alphahuman_config().await?;
let status = service::stop(&config).map_err(|e| e.to_string())?;
to_json_value(command_response(
status,
vec!["service stop completed".to_string()],
))
}
"alphahuman.service_status" => {
let config = load_alphahuman_config().await?;
let status = service::status(&config).map_err(|e| e.to_string())?;
to_json_value(command_response(
status,
vec!["service status fetched".to_string()],
))
}
"alphahuman.service_uninstall" => {
let config = load_alphahuman_config().await?;
let status = service::uninstall(&config).map_err(|e| e.to_string())?;
to_json_value(command_response(
status,
vec!["service uninstall completed".to_string()],
))
}
_ => Err(format!("unknown method: {method}")),
}
}
async fn health_handler() -> impl IntoResponse {
(StatusCode::OK, Json(json!({ "ok": true })))
}
async fn root_handler() -> impl IntoResponse {
(
StatusCode::OK,
Json(json!({
"name": "alphahuman-core",
"ok": true,
"endpoints": {
"health": "/health",
"rpc": "/rpc"
},
"usage": {
"jsonrpc": {
"version": "2.0",
"method": "core.ping",
"params": {}
}
}
})),
)
}
async fn not_found_handler() -> impl IntoResponse {
(
StatusCode::NOT_FOUND,
Json(json!({
"ok": false,
"error": "not_found",
"message": "Route not found. Try /, /health, or /rpc."
})),
)
}
fn core_port() -> u16 {
std::env::var("ALPHAHUMAN_CORE_PORT")
.ok()
.and_then(|v| v.parse::<u16>().ok())
.unwrap_or(7788)
}
pub async fn run_server(port: Option<u16>) -> Result<()> {
let port = port.unwrap_or_else(core_port);
let bind_addr = format!("127.0.0.1:{port}");
let listener = tokio::net::TcpListener::bind(&bind_addr).await?;
let app = Router::new()
.route("/", get(root_handler))
.route("/health", get(health_handler))
.route("/rpc", post(rpc_handler))
.fallback(not_found_handler)
.with_state(AppState {
core_version: env!("CARGO_PKG_VERSION").to_string(),
});
log::info!("[core] listening on http://{bind_addr}");
axum::serve(listener, app).await?;
Ok(())
}
pub fn run_from_cli_args(args: &[String]) -> Result<()> {
let mut port: Option<u16> = None;
let mut idx = 0;
while idx < args.len() {
match args[idx].as_str() {
"serve" => {}
"--port" => {
let value = args
.get(idx + 1)
.ok_or_else(|| anyhow::anyhow!("missing value for --port"))?;
port = Some(value.parse::<u16>()?);
idx += 1;
}
_ => {}
}
idx += 1;
}
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
runtime.block_on(run_server(port))
}
+70 -17
View File
@@ -9,26 +9,31 @@
//! - Native notifications
mod ai;
pub mod alphahuman;
mod auth;
mod commands;
mod core_process;
mod core_rpc;
pub mod core_server;
pub mod memory;
mod models;
mod runtime;
mod services;
pub mod alphahuman;
mod unified_skills;
mod utils;
use ai::*;
use commands::unified_skills::{
unified_execute_skill, unified_generate_skill, unified_list_skills,
unified_self_evolve_skill,
unified_execute_skill, unified_generate_skill, unified_list_skills, unified_self_evolve_skill,
};
use commands::*;
use services::socket_service::SOCKET_SERVICE;
use std::path::PathBuf;
use tauri::{AppHandle, Manager, RunEvent, Emitter};
use tokio::{fs, time::{interval, Duration}};
use tauri::{AppHandle, Emitter, Manager, RunEvent};
use tokio::{
fs,
time::{interval, Duration},
};
#[cfg(desktop)]
use tauri::{
@@ -51,8 +56,8 @@ async fn write_ai_config_file(filename: String, content: String) -> Result<bool,
use std::env;
// Get the project root directory (parent of src-tauri)
let current_dir = env::current_dir()
.map_err(|e| format!("Failed to get current directory: {e}"))?;
let current_dir =
env::current_dir().map_err(|e| format!("Failed to get current directory: {e}"))?;
// Go up one level to get project root (since we're in src-tauri/)
let project_root = current_dir
@@ -74,8 +79,7 @@ async fn write_ai_config_file(filename: String, content: String) -> Result<bool,
let file_path = ai_dir.join(&filename);
// Ensure ai directory exists
std::fs::create_dir_all(&ai_dir)
.map_err(|e| format!("Failed to create ai directory: {e}"))?;
std::fs::create_dir_all(&ai_dir).map_err(|e| format!("Failed to create ai directory: {e}"))?;
// Write the file
std::fs::write(&file_path, content)
@@ -277,7 +281,10 @@ async fn watch_daemon_health_file(app_handle: AppHandle, data_dir: PathBuf) {
let mut interval = interval(Duration::from_secs(2));
let mut last_modified: Option<std::time::SystemTime> = None;
log::info!("[alphahuman] Watching daemon health file: {}", state_file.display());
log::info!(
"[alphahuman] Watching daemon health file: {}",
state_file.display()
);
loop {
interval.tick().await;
@@ -290,26 +297,44 @@ async fn watch_daemon_health_file(app_handle: AppHandle, data_dir: PathBuf) {
// Read and parse health data
if let Ok(content) = fs::read_to_string(&state_file).await {
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&content) {
log::debug!("[alphahuman] Broadcasting health event from file: {:?}", json_value);
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&content)
{
log::debug!(
"[alphahuman] Broadcasting health event from file: {:?}",
json_value
);
// Emit Tauri event to frontend (same as internal daemon)
if let Err(e) = app_handle.emit("alphahuman:health", &json_value) {
log::error!("[alphahuman] Failed to emit health event from file: {}", e);
log::error!(
"[alphahuman] Failed to emit health event from file: {}",
e
);
} else {
log::debug!("[alphahuman] Health event emitted successfully from file");
log::debug!(
"[alphahuman] Health event emitted successfully from file"
);
}
} else {
log::debug!("[alphahuman] Failed to parse health file as JSON: {}", state_file.display());
log::debug!(
"[alphahuman] Failed to parse health file as JSON: {}",
state_file.display()
);
}
} else {
log::debug!("[alphahuman] Failed to read health file: {}", state_file.display());
log::debug!(
"[alphahuman] Failed to read health file: {}",
state_file.display()
);
}
}
}
} else {
// File doesn't exist yet - external daemon may not be writing yet
log::debug!("[alphahuman] Health file not found yet: {}", state_file.display());
log::debug!(
"[alphahuman] Health file not found yet: {}",
state_file.display()
);
}
}
}
@@ -638,6 +663,21 @@ pub fn run() {
}
}
// Start/ensure standalone core process for business logic RPC.
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
let core_handle = core_process::CoreProcessHandle::new(core_process::default_core_port());
std::env::set_var("ALPHAHUMAN_CORE_RPC_URL", core_handle.rpc_url());
app.manage(core_handle.clone());
tauri::async_runtime::spawn(async move {
if let Err(err) = core_handle.ensure_running().await {
log::error!("[core] failed to start core process: {err}");
} else {
log::info!("[core] core process ready");
}
});
}
if daemon_mode {
if let Some(window) = app.get_webview_window("main") {
let _ = window.hide();
@@ -789,6 +829,7 @@ pub fn run() {
alphahuman_service_stop,
alphahuman_service_status,
alphahuman_service_uninstall,
alphahuman_agent_server_status,
// Unified skill registry commands
unified_list_skills,
unified_execute_skill,
@@ -909,6 +950,7 @@ pub fn run() {
alphahuman_service_stop,
alphahuman_service_status,
alphahuman_service_uninstall,
alphahuman_agent_server_status,
// Unified skill registry commands (mobile stubs)
unified_list_skills,
unified_execute_skill,
@@ -951,6 +993,13 @@ pub fn run() {
log::info!("[alphahuman] Daemon shutdown signalled");
}
if let Some(core) = app_handle.try_state::<core_process::CoreProcessHandle>() {
let core_handle: core_process::CoreProcessHandle = (*core).clone();
tauri::async_runtime::spawn(async move {
core_handle.shutdown().await;
});
}
let _ = app_handle;
}
@@ -960,3 +1009,7 @@ pub fn run() {
}
});
}
pub fn run_core_from_args(args: &[String]) -> anyhow::Result<()> {
core_server::run_from_cli_args(args)
}
+9
View File
@@ -2,5 +2,14 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.get(1).map(String::as_str) == Some("core") {
if let Err(err) = alphahuman::run_core_from_args(&args[2..]) {
eprintln!("core process failed: {err}");
std::process::exit(1);
}
return;
}
alphahuman::run()
}
+1 -1
View File
@@ -51,7 +51,7 @@
"deep-link": {
"desktop": {
"schemes": [
"alphahuman"
"openhuman"
]
}
}
+16 -16
View File
@@ -67,21 +67,21 @@ const navItems = [
</svg>
),
},
{
id: 'invites',
label: 'Invite Friends',
path: '/invites',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"
/>
</svg>
),
},
// {
// id: 'invites',
// label: 'Invite Friends',
// path: '/invites',
// icon: (
// <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
// <path
// strokeLinecap="round"
// strokeLinejoin="round"
// strokeWidth={2}
// d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"
// />
// </svg>
// ),
// },
{
id: 'settings',
label: 'Settings',
@@ -178,7 +178,7 @@ const MiniSidebar = () => {
</div>
{/* Tooltip */}
<div className="pointer-events-none absolute left-full top-1/2 -translate-y-1/2 ml-2 px-2 py-1 bg-stone-800 text-white text-xs rounded-lg whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity duration-150">
Daemon Status
Agent Status
</div>
</div>
)}
+8 -8
View File
@@ -1,8 +1,8 @@
/**
* Daemon Health Panel
* Agent Health Panel
*
* Detailed health breakdown component showing daemon status, component health,
* and providing manual control buttons for daemon lifecycle management.
* Detailed health breakdown component showing agent status, component health,
* and providing manual control buttons for agent lifecycle management.
*/
import {
ArrowPathIcon,
@@ -29,13 +29,13 @@ const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
const daemonHealth = useDaemonHealth(userId);
const [operationLoading, setOperationLoading] = useState<string | null>(null);
// Handle daemon operations with loading states
// Handle agent operations with loading states
const handleOperation = async (operation: () => Promise<unknown>, operationName: string) => {
setOperationLoading(operationName);
try {
await operation();
} catch (error) {
console.error(`[DaemonHealthPanel] ${operationName} failed:`, error);
console.error(`[AgentHealthPanel] ${operationName} failed:`, error);
} finally {
setOperationLoading(null);
}
@@ -87,7 +87,7 @@ const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
<div className={`bg-stone-900 rounded-lg border border-stone-700 p-6 space-y-6 ${className}`}>
{/* Header */}
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-white">Daemon Health</h3>
<h3 className="text-lg font-semibold text-white">Agent Status</h3>
{onClose && (
<button onClick={onClose} className="text-gray-400 hover:text-white transition-colors">
<XMarkIcon className="w-5 h-5" />
@@ -172,8 +172,8 @@ const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
{/* Auto-start Toggle */}
<div className="flex items-center justify-between p-3 rounded-lg bg-stone-800/40 border border-stone-700/60">
<div>
<div className="text-sm font-medium text-gray-300">Auto-start Daemon</div>
<div className="text-xs text-gray-500">Automatically start daemon on app launch</div>
<div className="text-sm font-medium text-gray-300">Auto-start Agent</div>
<div className="text-xs text-gray-500">Automatically start agent on app launch</div>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
+49 -47
View File
@@ -116,41 +116,41 @@ const SettingsHome = () => {
onClick: () => navigateToSettings('ai'),
dangerous: false,
},
{
id: 'agent-chat',
title: 'Agent Chat',
description: 'Send messages directly to your agent',
devOnly: true,
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 10h8m-8 4h5m-6 6l-4 4V6a2 2 0 012-2h12a2 2 0 012 2v9a2 2 0 01-2 2H7z"
/>
</svg>
),
onClick: () => navigateToSettings('agent-chat'),
dangerous: false,
},
{
id: 'privacy',
title: 'Privacy & Security',
description: 'Control your privacy and security settings',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
),
onClick: () => navigateToSettings('privacy'),
dangerous: false,
},
// {
// id: 'agent-chat',
// title: 'Agent Chat',
// description: 'Send messages directly to your agent',
// devOnly: true,
// icon: (
// <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
// <path
// strokeLinecap="round"
// strokeLinejoin="round"
// strokeWidth={2}
// d="M8 10h8m-8 4h5m-6 6l-4 4V6a2 2 0 012-2h12a2 2 0 012 2v9a2 2 0 01-2 2H7z"
// />
// </svg>
// ),
// onClick: () => navigateToSettings('agent-chat'),
// dangerous: false,
// },
// {
// id: 'privacy',
// title: 'Privacy & Security',
// description: 'Control your privacy and security settings',
// icon: (
// <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
// <path
// strokeLinecap="round"
// strokeLinejoin="round"
// strokeWidth={2}
// d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
// />
// </svg>
// ),
// onClick: () => navigateToSettings('privacy'),
// dangerous: false,
// },
// {
// id: 'profile',
// title: 'Profile',
@@ -325,18 +325,20 @@ const SettingsHome = () => {
<div className="p-4 space-y-6">
{/* Main Settings */}
<div>
{mainMenuItems.filter(item => !item.devOnly || IS_DEV).map((item, index, filtered) => (
<SettingsMenuItem
key={item.id}
icon={item.icon}
title={item.title}
description={item.description}
onClick={item.onClick}
dangerous={item.dangerous}
isFirst={index === 0}
isLast={index === filtered.length - 1}
/>
))}
{mainMenuItems
.filter(item => !item.devOnly || IS_DEV)
.map((item, index, filtered) => (
<SettingsMenuItem
key={item.id}
icon={item.icon}
title={item.title}
description={item.description}
onClick={item.onClick}
dangerous={item.dangerous}
isFirst={index === 0}
isLast={index === filtered.length - 1}
/>
))}
</div>
{/* Destructive Actions */}
@@ -974,7 +974,7 @@ const TauriCommandsPanel = () => {
</div>
)}
<InputGroup title="Daemon Service Management">
<InputGroup title="Agent Service Management">
<div className="md:col-span-2">
<div className="space-y-4">
{/* Live Status Display */}
@@ -983,7 +983,7 @@ const TauriCommandsPanel = () => {
<DaemonHealthIndicator size="md" />
<div>
<div className="text-white font-medium">
Daemon Status: {daemonHealth.status}
Agent Status: {daemonHealth.status}
</div>
<div className="text-xs text-gray-400">
Last update:{' '}
@@ -1074,9 +1074,9 @@ const TauriCommandsPanel = () => {
{/* Auto-start Toggle */}
<div className="flex items-center justify-between p-3 rounded-lg bg-stone-800/40 border border-stone-700/60">
<div>
<div className="text-sm font-medium text-gray-300">Auto-start Daemon</div>
<div className="text-sm font-medium text-gray-300">Auto-start Agent</div>
<div className="text-xs text-gray-500">
Automatically start daemon on app launch
Automatically start agent on app launch
</div>
</div>
<label className="relative inline-flex items-center cursor-pointer">
+67 -17
View File
@@ -4,7 +4,7 @@
* React hook for accessing daemon health state and actions.
* Provides convenient access to daemon status, components, and control functions.
*/
import { useCallback } from 'react';
import { useCallback, useEffect } from 'react';
import {
resetConnectionAttempts,
@@ -16,10 +16,12 @@ import {
selectIsDaemonAutoStartEnabled,
selectIsDaemonRecovering,
setAutoStartEnabled,
setDaemonStatus,
setIsRecovering,
} from '../store/daemonSlice';
import { useAppDispatch, useAppSelector } from '../store/hooks';
import {
alphahumanAgentServerStatus,
alphahumanServiceStart,
alphahumanServiceStatus,
alphahumanServiceStop,
@@ -38,51 +40,90 @@ export const useDaemonHealth = (userId?: string) => {
const isAutoStartEnabled = useAppSelector(state => selectIsDaemonAutoStartEnabled(state, userId));
const connectionAttempts = useAppSelector(state => selectDaemonConnectionAttempts(state, userId));
const isRecovering = useAppSelector(state => selectIsDaemonRecovering(state, userId));
const uid = userId || '__pending__';
const probeAgentStatus = useCallback(async (): Promise<boolean> => {
try {
const result = await alphahumanAgentServerStatus();
const running = !!result?.result?.running;
dispatch(setDaemonStatus({ userId: uid, status: running ? 'running' : 'disconnected' }));
return running;
} catch (error) {
console.error('[useDaemonHealth] Failed to probe agent status:', error);
dispatch(setDaemonStatus({ userId: uid, status: 'disconnected' }));
return false;
}
}, [dispatch, uid]);
const waitForAgentStatus = useCallback(
async (targetRunning: boolean, timeoutMs = 10000): Promise<boolean> => {
const startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) {
const running = await probeAgentStatus();
if (running === targetRunning) {
return true;
}
await new Promise(resolve => setTimeout(resolve, 500));
}
return false;
},
[probeAgentStatus]
);
// Action creators
const startDaemon = useCallback(async (): Promise<CommandResponse<ServiceStatus> | null> => {
try {
dispatch(setDaemonStatus({ userId: uid, status: 'starting' }));
const result = await alphahumanServiceStart();
// Check if the service status indicates success
if (result.result && result.result.state === 'Running') {
dispatch(resetConnectionAttempts({ userId: userId || '__pending__' }));
const running = await waitForAgentStatus(true);
if (running) {
if (result?.result) {
(result.result as { state?: string }).state = 'Running';
}
dispatch(resetConnectionAttempts({ userId: uid }));
} else {
dispatch(setDaemonStatus({ userId: uid, status: 'error' }));
}
return result;
} catch (error) {
console.error('[useDaemonHealth] Failed to start daemon:', error);
dispatch(setDaemonStatus({ userId: uid, status: 'error' }));
return null;
}
}, [dispatch, userId]);
}, [dispatch, uid, waitForAgentStatus]);
const stopDaemon = useCallback(async (): Promise<CommandResponse<ServiceStatus> | null> => {
try {
return await alphahumanServiceStop();
dispatch(setDaemonStatus({ userId: uid, status: 'starting' }));
const result = await alphahumanServiceStop();
await waitForAgentStatus(false, 7000);
return result;
} catch (error) {
console.error('[useDaemonHealth] Failed to stop daemon:', error);
return null;
}
}, []);
}, [dispatch, uid, waitForAgentStatus]);
const restartDaemon = useCallback(async (): Promise<boolean> => {
const uid = userId || '__pending__';
try {
dispatch(setIsRecovering({ userId: uid, isRecovering: true }));
dispatch(setDaemonStatus({ userId: uid, status: 'starting' }));
// Stop first
const stopResult = await alphahumanServiceStop();
if (!stopResult?.result || stopResult.result.state !== 'Stopped') {
console.warn('[useDaemonHealth] Stop daemon failed, but continuing with start');
}
await alphahumanServiceStop();
await waitForAgentStatus(false, 7000);
// Wait a moment for clean shutdown
await new Promise(resolve => setTimeout(resolve, 2000));
// Start again
const startResult = await alphahumanServiceStart();
const success = startResult?.result && startResult.result.state === 'Running';
await alphahumanServiceStart();
const success = await waitForAgentStatus(true, 12000);
if (success) {
dispatch(resetConnectionAttempts({ userId: uid }));
} else {
dispatch(setDaemonStatus({ userId: uid, status: 'error' }));
}
dispatch(setIsRecovering({ userId: uid, isRecovering: false }));
@@ -90,19 +131,24 @@ export const useDaemonHealth = (userId?: string) => {
} catch (error) {
console.error('[useDaemonHealth] Failed to restart daemon:', error);
dispatch(setIsRecovering({ userId: uid, isRecovering: false }));
dispatch(setDaemonStatus({ userId: uid, status: 'error' }));
return false;
}
}, [dispatch, userId]);
}, [dispatch, uid, waitForAgentStatus]);
const checkDaemonStatus =
useCallback(async (): Promise<CommandResponse<ServiceStatus> | null> => {
try {
return await alphahumanServiceStatus();
const running = await probeAgentStatus();
if (running) {
return await alphahumanServiceStatus();
}
return null;
} catch (error) {
console.error('[useDaemonHealth] Failed to check daemon status:', error);
return null;
}
}, []);
}, [probeAgentStatus]);
const setAutoStart = useCallback(
(enabled: boolean) => {
@@ -124,6 +170,10 @@ export const useDaemonHealth = (userId?: string) => {
// Get uptime in human readable format
const uptimeText = healthSnapshot ? formatUptime(healthSnapshot.uptime_seconds) : 'Unknown';
useEffect(() => {
void probeAgentStatus();
}, [probeAgentStatus]);
return {
// State
status,
+10 -57
View File
@@ -1,14 +1,11 @@
import { useNavigate } from 'react-router-dom';
import ConnectionIndicator from '../components/ConnectionIndicator';
import SkillsGrid from '../components/SkillsGrid';
import { useUser } from '../hooks/useUser';
import { TELEGRAM_BOT_USERNAME } from '../utils/config';
import { openUrl } from '../utils/openUrl';
const Home = () => {
const navigate = useNavigate();
const { user } = useUser();
const navigate = useNavigate();
const userName = user?.firstName || 'User';
// Get greeting based on time
@@ -19,18 +16,11 @@ const Home = () => {
return 'Good evening';
};
// Handle Telegram bot link
// Open in-app conversations window
const handleStartCooking = async () => {
await openUrl(`https://t.me/${TELEGRAM_BOT_USERNAME}`);
navigate('/conversations');
};
const handleUpgrade = () => {
navigate('/settings/billing');
};
const currentPlan = user?.subscription?.plan || 'FREE';
const showUpgradeCTA = currentPlan === 'FREE';
return (
<div className="min-h-full relative">
{/* Content overlay */}
@@ -38,48 +28,6 @@ const Home = () => {
{/* Main content */}
<div className="flex-1 flex items-center justify-center p-4">
<div className="max-w-md w-full">
{/* Upgrade CTA */}
{showUpgradeCTA && (
<button
onClick={handleUpgrade}
className="glass rounded-3xl p-4 shadow-large animate-fade-up mb-4 w-full text-left hover:bg-stone-800/30 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-primary-500/50 group">
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<svg
className="w-5 h-5 text-primary-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
<span className="font-semibold text-sm">Upgrade to Premium</span>
</div>
<p className="text-xs opacity-70">
Unlock advanced features and unlimited access
</p>
</div>
<svg
className="w-5 h-5 opacity-60 group-hover:opacity-100 transition-opacity"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</div>
</button>
)}
{/* Weather card */}
<div className="glass rounded-3xl p-4 shadow-large animate-fade-up text-center">
{/* Greeting */}
@@ -98,8 +46,13 @@ const Home = () => {
</button>
</div>
{/* Skills Grid */}
<SkillsGrid />
<div className="mt-4 mb-8">
<button
onClick={() => navigate('/skills')}
className="btn-secondary w-full py-2.5 text-sm font-medium rounded-xl">
Open Skills Page
</button>
</div>
</div>
</div>
</div>
+6 -8
View File
@@ -1,7 +1,9 @@
import { Link } from 'react-router-dom';
import OAuthProviderButton from '../components/oauth/OAuthProviderButton';
import { getProviderConfig } from '../components/oauth/providerConfigs';
import RotatingTetrahedronCanvas from '../components/RotatingTetrahedronCanvas';
const googleProvider = getProviderConfig('google');
const Welcome = () => {
return (
<div className="min-h-full w-full bg-[#090b12] px-4 py-6 md:px-10 md:py-10">
@@ -20,12 +22,8 @@ const Welcome = () => {
Your AI superhuman for personal and business life.
</p>
<div className="flex flex-wrap items-center justify-center gap-3">
<Link
className="inline-flex bg-[#201732] px-5 py-2 text-sm font-medium tracking-wide text-[#d4c8ff] transition-colors hover:bg-[#2a1d44]"
to="/login">
Get Started
</Link>
<div className="flex w-full max-w-sm flex-col items-stretch">
{googleProvider ? <OAuthProviderButton provider={googleProvider} /> : null}
</div>
</div>
</section>
-3
View File
@@ -63,9 +63,6 @@ const Onboarding = () => {
return (
<div className="min-h-full relative flex items-center justify-center">
<div className="relative z-10 max-w-lg w-full mx-4">
<div className="flex justify-center mb-6">
<LottieAnimation src={stepAnimations[currentStep - 1]} height={120} width={120} />
</div>
<ProgressIndicator currentStep={currentStep} totalSteps={totalSteps} />
{renderStep()}
</div>
+2 -2
View File
@@ -26,8 +26,8 @@ const FeaturesStep = ({ onNext }: FeaturesStepProps) => {
return (
<div className="glass rounded-3xl p-8 shadow-large animate-fade-up">
<div className="text-center mb-4">
<h1 className="text-xl font-bold mb-2">Are You Ready For This?</h1>
<p className="opacity-70 text-sm">Here's some of the things that AlphaHuman can do</p>
<h1 className="text-xl font-bold mb-2">Ready to get started?</h1>
<p className="opacity-70 text-sm">Here's some of the things that OpenHuman can do</p>
</div>
<div className="space-y-2 mb-4">
+3 -4
View File
@@ -108,10 +108,7 @@ class ApiClient {
// Handle abort/timeout specifically
if (error instanceof DOMException && error.name === 'AbortError') {
throw {
success: false,
error: `Request timed out after ${timeout / 1000}s`,
} as ApiError;
throw { success: false, error: `Request timed out after ${timeout / 1000}s` } as ApiError;
}
// Wrap network/other errors
@@ -174,3 +171,5 @@ class ApiClient {
// Export singleton instance
export const apiClient = new ApiClient(BACKEND_URL);
console.log('[ApiClient] Backend URL', BACKEND_URL);
+1 -1
View File
@@ -1,4 +1,4 @@
export const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.alphahuman.xyz';
export const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.tinyhumans.ai';
export const TELEGRAM_BOT_USERNAME =
import.meta.env.VITE_TELEGRAM_BOT_USERNAME || 'alphahumanx_bot';
+2 -2
View File
@@ -46,7 +46,7 @@ const handleAuthDeepLink = async (parsed: URL) => {
return;
}
console.log('[DeepLink] Received auth token');
console.log('[DeepLink] Received auth token', token);
try {
await invoke('show_window');
@@ -271,7 +271,7 @@ const handleDeepLinkUrls = async (urls: string[] | null | undefined) => {
try {
const parsed = new URL(url);
if (parsed.protocol !== 'alphahuman:') {
if (parsed.protocol !== 'alphahuman:' && parsed.protocol !== 'openhuman:') {
return;
}
+12
View File
@@ -287,6 +287,11 @@ export interface ServiceStatus {
details?: string | null;
}
export interface AgentServerStatus {
running: boolean;
url: string;
}
export interface ConfigSnapshot {
config: Record<string, unknown>;
workspace_dir: string;
@@ -564,6 +569,13 @@ export async function alphahumanServiceUninstall(): Promise<CommandResponse<Serv
return await invoke('alphahuman_service_uninstall');
}
export async function alphahumanAgentServerStatus(): Promise<CommandResponse<AgentServerStatus>> {
if (!isTauri()) {
throw new Error('Not running in Tauri');
}
return await invoke('alphahuman_agent_server_status');
}
export async function runtimeListSkills(): Promise<SkillSnapshot[]> {
if (!isTauri()) {
throw new Error('Not running in Tauri');