mirror of
https://github.com/RightNow-AI/openfang.git
synced 2026-07-30 23:05:08 +00:00
bugfixes batch
This commit is contained in:
Generated
+14
-14
@@ -3866,7 +3866,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-api"
|
||||
version = "0.3.4"
|
||||
version = "0.3.5"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3902,7 +3902,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-channels"
|
||||
version = "0.3.4"
|
||||
version = "0.3.5"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3933,7 +3933,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-cli"
|
||||
version = "0.3.4"
|
||||
version = "0.3.5"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"clap_complete",
|
||||
@@ -3960,7 +3960,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-desktop"
|
||||
version = "0.3.4"
|
||||
version = "0.3.5"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"open",
|
||||
@@ -3986,7 +3986,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-extensions"
|
||||
version = "0.3.4"
|
||||
version = "0.3.5"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"argon2",
|
||||
@@ -4014,7 +4014,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-hands"
|
||||
version = "0.3.4"
|
||||
version = "0.3.5"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dashmap",
|
||||
@@ -4031,7 +4031,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-kernel"
|
||||
version = "0.3.4"
|
||||
version = "0.3.5"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -4067,7 +4067,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-memory"
|
||||
version = "0.3.4"
|
||||
version = "0.3.5"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -4086,7 +4086,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-migrate"
|
||||
version = "0.3.4"
|
||||
version = "0.3.5"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dirs 6.0.0",
|
||||
@@ -4105,7 +4105,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-runtime"
|
||||
version = "0.3.4"
|
||||
version = "0.3.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -4137,7 +4137,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-skills"
|
||||
version = "0.3.4"
|
||||
version = "0.3.5"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"hex",
|
||||
@@ -4160,7 +4160,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-types"
|
||||
version = "0.3.4"
|
||||
version = "0.3.5"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -4179,7 +4179,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openfang-wire"
|
||||
version = "0.3.4"
|
||||
version = "0.3.5"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -8791,7 +8791,7 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.3.4"
|
||||
version = "0.3.5"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.3.5"
|
||||
version = "0.3.6"
|
||||
edition = "2021"
|
||||
license = "Apache-2.0 OR MIT"
|
||||
repository = "https://github.com/RightNow-AI/openfang"
|
||||
|
||||
@@ -3614,6 +3614,42 @@ pub async fn install_hand_deps(
|
||||
)
|
||||
}
|
||||
|
||||
/// POST /api/hands/install — Install a hand from TOML content.
|
||||
pub async fn install_hand(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(body): Json<serde_json::Value>,
|
||||
) -> impl IntoResponse {
|
||||
let toml_content = body["toml_content"].as_str().unwrap_or("");
|
||||
let skill_content = body["skill_content"].as_str().unwrap_or("");
|
||||
|
||||
if toml_content.is_empty() {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({"error": "Missing toml_content field"})),
|
||||
);
|
||||
}
|
||||
|
||||
match state
|
||||
.kernel
|
||||
.hand_registry
|
||||
.install_from_content(toml_content, skill_content)
|
||||
{
|
||||
Ok(def) => (
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"id": def.id,
|
||||
"name": def.name,
|
||||
"description": def.description,
|
||||
"category": format!("{:?}", def.category),
|
||||
})),
|
||||
),
|
||||
Err(e) => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({"error": format!("{e}")})),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// POST /api/hands/{hand_id}/activate — Activate a hand (spawns agent).
|
||||
pub async fn activate_hand(
|
||||
State(state): State<Arc<AppState>>,
|
||||
|
||||
@@ -331,6 +331,10 @@ pub async fn build_router(
|
||||
)
|
||||
// Hands endpoints
|
||||
.route("/api/hands", axum::routing::get(routes::list_hands))
|
||||
.route(
|
||||
"/api/hands/install",
|
||||
axum::routing::post(routes::install_hand),
|
||||
)
|
||||
.route(
|
||||
"/api/hands/active",
|
||||
axum::routing::get(routes::list_active_hands),
|
||||
|
||||
@@ -441,8 +441,8 @@ function wizardPage() {
|
||||
toml += 'description = "' + tpl.description.replace(/"/g, '\\"') + '"\n';
|
||||
toml += 'profile = "' + tpl.profile + '"\n\n';
|
||||
toml += '[model]\nprovider = "' + provider + '"\n';
|
||||
toml += 'name = "' + model + '"\n\n';
|
||||
toml += '[prompt]\nsystem = """\n' + tpl.system_prompt + '\n"""\n';
|
||||
toml += 'model = "' + model + '"\n';
|
||||
toml += 'system_prompt = """\n' + tpl.system_prompt + '\n"""\n';
|
||||
|
||||
this.creatingAgent = true;
|
||||
try {
|
||||
|
||||
@@ -79,8 +79,13 @@ impl TelegramAdapter {
|
||||
self.token.as_str()
|
||||
);
|
||||
|
||||
// Sanitize: strip unsupported HTML tags so Telegram doesn't reject with 400.
|
||||
// Telegram only allows: b, i, u, s, tg-spoiler, a, code, pre, blockquote.
|
||||
// Any other tag (e.g. <name>, <thinking>) causes a 400 Bad Request.
|
||||
let sanitized = sanitize_telegram_html(text);
|
||||
|
||||
// Telegram has a 4096 character limit per message — split if needed
|
||||
let chunks = split_message(text, 4096);
|
||||
let chunks = split_message(&sanitized, 4096);
|
||||
for chunk in chunks {
|
||||
let body = serde_json::json!({
|
||||
"chat_id": chat_id,
|
||||
@@ -524,6 +529,57 @@ pub fn calculate_backoff(current: Duration) -> Duration {
|
||||
(current * 2).min(MAX_BACKOFF)
|
||||
}
|
||||
|
||||
/// Sanitize text for Telegram HTML parse mode.
|
||||
///
|
||||
/// Escapes angle brackets that are NOT part of Telegram-allowed HTML tags.
|
||||
/// Allowed tags: b, i, u, s, tg-spoiler, a, code, pre, blockquote.
|
||||
/// Everything else (e.g. `<name>`, `<thinking>`) gets escaped to `<...>`.
|
||||
fn sanitize_telegram_html(text: &str) -> String {
|
||||
const ALLOWED: &[&str] = &[
|
||||
"b", "i", "u", "s", "em", "strong", "a", "code", "pre", "blockquote", "tg-spoiler",
|
||||
"tg-emoji",
|
||||
];
|
||||
|
||||
let mut result = String::with_capacity(text.len());
|
||||
let bytes = text.as_bytes();
|
||||
let mut i = 0;
|
||||
|
||||
while i < bytes.len() {
|
||||
if bytes[i] == b'<' {
|
||||
// Try to parse an HTML tag
|
||||
if let Some(end) = text[i..].find('>') {
|
||||
let tag_content = &text[i + 1..i + end]; // content between < and >
|
||||
let tag_name = tag_content
|
||||
.trim_start_matches('/')
|
||||
.split(|c: char| c.is_whitespace() || c == '/' || c == '>')
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.to_lowercase();
|
||||
|
||||
if !tag_name.is_empty() && ALLOWED.contains(&tag_name.as_str()) {
|
||||
// Allowed tag — keep as-is
|
||||
result.push_str(&text[i..i + end + 1]);
|
||||
} else {
|
||||
// Unknown tag — escape both brackets
|
||||
result.push_str("<");
|
||||
result.push_str(&text[i + 1..i + end]);
|
||||
result.push_str(">");
|
||||
}
|
||||
i += end + 1;
|
||||
} else {
|
||||
// No closing > — escape the lone <
|
||||
result.push_str("<");
|
||||
i += 1;
|
||||
}
|
||||
} else {
|
||||
result.push(text[i..].chars().next().unwrap());
|
||||
i += text[i..].chars().next().unwrap().len_utf8();
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -6,9 +6,17 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Get the OpenFang home directory, respecting OPENFANG_HOME env var.
|
||||
fn dotenv_openfang_home() -> Option<PathBuf> {
|
||||
if let Ok(home) = std::env::var("OPENFANG_HOME") {
|
||||
return Some(PathBuf::from(home));
|
||||
}
|
||||
dirs::home_dir().map(|h| h.join(".openfang"))
|
||||
}
|
||||
|
||||
/// Return the path to `~/.openfang/.env`.
|
||||
pub fn env_file_path() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|h| h.join(".openfang").join(".env"))
|
||||
dotenv_openfang_home().map(|h| h.join(".env"))
|
||||
}
|
||||
|
||||
/// Load `~/.openfang/.env` and `~/.openfang/secrets.env` into `std::env`.
|
||||
@@ -25,7 +33,7 @@ pub fn load_dotenv() {
|
||||
|
||||
/// Return the path to `~/.openfang/secrets.env`.
|
||||
pub fn secrets_env_path() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|h| h.join(".openfang").join("secrets.env"))
|
||||
dotenv_openfang_home().map(|h| h.join("secrets.env"))
|
||||
}
|
||||
|
||||
fn load_env_file(path: Option<PathBuf>) {
|
||||
|
||||
@@ -39,11 +39,15 @@ fn detect_provider() -> Option<(&'static str, &'static str)> {
|
||||
}
|
||||
|
||||
fn is_first_run() -> bool {
|
||||
let home = match dirs::home_dir() {
|
||||
Some(h) => h,
|
||||
None => return true,
|
||||
let of_home = if let Ok(h) = std::env::var("OPENFANG_HOME") {
|
||||
std::path::PathBuf::from(h)
|
||||
} else {
|
||||
match dirs::home_dir() {
|
||||
Some(h) => h.join(".openfang"),
|
||||
None => return true,
|
||||
}
|
||||
};
|
||||
!home.join(".openfang").join("config.toml").exists()
|
||||
!of_home.join("config.toml").exists()
|
||||
}
|
||||
|
||||
fn has_openclaw() -> bool {
|
||||
|
||||
@@ -390,6 +390,11 @@ enum HandCommands {
|
||||
List,
|
||||
/// Show currently active hand instances.
|
||||
Active,
|
||||
/// Install a hand from a local directory containing HAND.toml.
|
||||
Install {
|
||||
/// Path to the hand directory (must contain HAND.toml).
|
||||
path: String,
|
||||
},
|
||||
/// Activate a hand by ID.
|
||||
Activate {
|
||||
/// Hand ID (e.g. "clip", "lead", "researcher").
|
||||
@@ -781,11 +786,19 @@ fn init_tracing_stderr() {
|
||||
.init();
|
||||
}
|
||||
|
||||
/// Get the OpenFang home directory, respecting OPENFANG_HOME env var.
|
||||
fn cli_openfang_home() -> std::path::PathBuf {
|
||||
if let Ok(home) = std::env::var("OPENFANG_HOME") {
|
||||
return std::path::PathBuf::from(home);
|
||||
}
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(std::env::temp_dir)
|
||||
.join(".openfang")
|
||||
}
|
||||
|
||||
/// Redirect tracing to a log file so it doesn't corrupt the ratatui TUI.
|
||||
fn init_tracing_file() {
|
||||
let log_dir = dirs::home_dir()
|
||||
.map(|h| h.join(".openfang"))
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."));
|
||||
let log_dir = cli_openfang_home();
|
||||
let _ = std::fs::create_dir_all(&log_dir);
|
||||
let log_path = log_dir.join("tui.log");
|
||||
|
||||
@@ -910,6 +923,7 @@ fn main() {
|
||||
Some(Commands::Hand(sub)) => match sub {
|
||||
HandCommands::List => cmd_hand_list(),
|
||||
HandCommands::Active => cmd_hand_active(),
|
||||
HandCommands::Install { path } => cmd_hand_install(&path),
|
||||
HandCommands::Activate { id } => cmd_hand_activate(&id),
|
||||
HandCommands::Deactivate { id } => cmd_hand_deactivate(&id),
|
||||
HandCommands::Info { id } => cmd_hand_info(&id),
|
||||
@@ -1037,7 +1051,7 @@ pub(crate) fn restrict_dir_permissions(path: &std::path::Path) {
|
||||
pub(crate) fn restrict_dir_permissions(_path: &std::path::Path) {}
|
||||
|
||||
pub(crate) fn find_daemon() -> Option<String> {
|
||||
let home_dir = dirs::home_dir()?.join(".openfang");
|
||||
let home_dir = cli_openfang_home();
|
||||
let info = read_daemon_info(&home_dir)?;
|
||||
|
||||
// Normalize listen address: replace 0.0.0.0 with 127.0.0.1 to avoid
|
||||
@@ -1119,7 +1133,7 @@ fn cmd_init(quick: bool) {
|
||||
}
|
||||
};
|
||||
|
||||
let openfang_dir = home.join(".openfang");
|
||||
let openfang_dir = cli_openfang_home();
|
||||
|
||||
// --- Ensure directories exist ---
|
||||
if !openfang_dir.exists() {
|
||||
@@ -1421,7 +1435,7 @@ fn cmd_start(config: Option<PathBuf>) {
|
||||
|
||||
/// Read the api_key from ~/.openfang/config.toml (if any).
|
||||
fn read_api_key() -> Option<String> {
|
||||
let config_path = dirs::home_dir()?.join(".openfang").join("config.toml");
|
||||
let config_path = cli_openfang_home().join("config.toml");
|
||||
let text = std::fs::read_to_string(config_path).ok()?;
|
||||
let table: toml::Value = text.parse().ok()?;
|
||||
let key = table.get("api_key")?.as_str()?;
|
||||
@@ -1451,8 +1465,8 @@ fn cmd_stop() {
|
||||
}
|
||||
}
|
||||
// Still alive — force kill via PID
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
let of_dir = home.join(".openfang");
|
||||
{
|
||||
let of_dir = cli_openfang_home();
|
||||
if let Some(info) = read_daemon_info(&of_dir) {
|
||||
force_kill_pid(info.pid);
|
||||
let _ = std::fs::remove_file(of_dir.join("daemon.json"));
|
||||
@@ -1927,8 +1941,8 @@ fn cmd_doctor(json: bool, repair: bool) {
|
||||
}
|
||||
|
||||
let home = dirs::home_dir();
|
||||
if let Some(h) = &home {
|
||||
let openfang_dir = h.join(".openfang");
|
||||
if let Some(_h) = &home {
|
||||
let openfang_dir = cli_openfang_home();
|
||||
|
||||
// --- Check 1: OpenFang directory ---
|
||||
if openfang_dir.exists() {
|
||||
@@ -2333,8 +2347,8 @@ decay_rate = 0.05
|
||||
}
|
||||
|
||||
// --- Check 11: .env keys vs config api_key_env consistency ---
|
||||
if let Some(ref h) = home {
|
||||
let openfang_dir = h.join(".openfang");
|
||||
{
|
||||
let openfang_dir = cli_openfang_home();
|
||||
let config_path = openfang_dir.join("config.toml");
|
||||
if config_path.exists() {
|
||||
let config_str = std::fs::read_to_string(&config_path).unwrap_or_default();
|
||||
@@ -2359,8 +2373,8 @@ decay_rate = 0.05
|
||||
}
|
||||
|
||||
// --- Check 12: Config deserialization into KernelConfig ---
|
||||
if let Some(ref h) = home {
|
||||
let openfang_dir = h.join(".openfang");
|
||||
{
|
||||
let openfang_dir = cli_openfang_home();
|
||||
let config_path = openfang_dir.join("config.toml");
|
||||
if config_path.exists() {
|
||||
if !json {
|
||||
@@ -2464,10 +2478,7 @@ decay_rate = 0.05
|
||||
if !json {
|
||||
println!("\n Skills:");
|
||||
}
|
||||
let skills_dir = home
|
||||
.as_ref()
|
||||
.map(|h| h.join(".openfang").join("skills"))
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("skills"));
|
||||
let skills_dir = cli_openfang_home().join("skills");
|
||||
let mut skill_reg = openfang_skills::registry::SkillRegistry::new(skills_dir.clone());
|
||||
skill_reg.load_bundled();
|
||||
let bundled_count = skill_reg.count();
|
||||
@@ -2528,11 +2539,11 @@ decay_rate = 0.05
|
||||
}
|
||||
|
||||
// --- Check 14: Extension registry health ---
|
||||
if let Some(ref h) = home {
|
||||
{
|
||||
if !json {
|
||||
println!("\n Extensions:");
|
||||
}
|
||||
let openfang_dir = h.join(".openfang");
|
||||
let openfang_dir = cli_openfang_home();
|
||||
let mut ext_registry =
|
||||
openfang_extensions::registry::IntegrationRegistry::new(&openfang_dir);
|
||||
ext_registry.load_bundled();
|
||||
@@ -3143,12 +3154,7 @@ fn cmd_migrate(args: MigrateArgs) {
|
||||
}
|
||||
});
|
||||
|
||||
let target_dir = dirs::home_dir()
|
||||
.unwrap_or_else(|| {
|
||||
eprintln!("Error: Could not determine home directory");
|
||||
std::process::exit(1);
|
||||
})
|
||||
.join(".openfang");
|
||||
let target_dir = cli_openfang_home();
|
||||
|
||||
println!("Migrating from {} ({})...", source, source_dir.display());
|
||||
if args.dry_run {
|
||||
@@ -3839,6 +3845,52 @@ fn cmd_channel_toggle(channel: &str, enable: bool) {
|
||||
// Hand commands
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn cmd_hand_install(path: &str) {
|
||||
let base = require_daemon("hand install");
|
||||
let dir = std::path::Path::new(path);
|
||||
let toml_path = dir.join("HAND.toml");
|
||||
let skill_path = dir.join("SKILL.md");
|
||||
|
||||
if !toml_path.exists() {
|
||||
eprintln!(
|
||||
"Error: No HAND.toml found in {}",
|
||||
dir.canonicalize()
|
||||
.unwrap_or_else(|_| dir.to_path_buf())
|
||||
.display()
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let toml_content = std::fs::read_to_string(&toml_path).unwrap_or_else(|e| {
|
||||
eprintln!("Error reading {}: {e}", toml_path.display());
|
||||
std::process::exit(1);
|
||||
});
|
||||
let skill_content = std::fs::read_to_string(&skill_path).unwrap_or_default();
|
||||
|
||||
let client = daemon_client();
|
||||
let body = daemon_json(
|
||||
client
|
||||
.post(format!("{base}/api/hands/install"))
|
||||
.json(&serde_json::json!({
|
||||
"toml_content": toml_content,
|
||||
"skill_content": skill_content,
|
||||
}))
|
||||
.send(),
|
||||
);
|
||||
|
||||
if let Some(err) = body.get("error").and_then(|v| v.as_str()) {
|
||||
eprintln!("Error: {err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
println!(
|
||||
"Installed hand: {} ({})",
|
||||
body["name"].as_str().unwrap_or("?"),
|
||||
body["id"].as_str().unwrap_or("?"),
|
||||
);
|
||||
println!("Use `openfang hand activate {}` to start it.", body["id"].as_str().unwrap_or("?"));
|
||||
}
|
||||
|
||||
fn cmd_hand_list() {
|
||||
let base = require_daemon("hand list");
|
||||
let client = daemon_client();
|
||||
@@ -4565,6 +4617,9 @@ fn cmd_quick_chat(config: Option<PathBuf>, agent: Option<String>) {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub(crate) fn openfang_home() -> PathBuf {
|
||||
if let Ok(home) = std::env::var("OPENFANG_HOME") {
|
||||
return PathBuf::from(home);
|
||||
}
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| {
|
||||
eprintln!("Error: Could not determine home directory");
|
||||
@@ -5407,9 +5462,7 @@ fn cmd_sessions(agent: Option<&str>, json: bool) {
|
||||
}
|
||||
|
||||
fn cmd_logs(lines: usize, follow: bool) {
|
||||
let log_path = dirs::home_dir()
|
||||
.map(|h| h.join(".openfang").join("tui.log"))
|
||||
.unwrap_or_else(|| PathBuf::from("tui.log"));
|
||||
let log_path = cli_openfang_home().join("tui.log");
|
||||
|
||||
if !log_path.exists() {
|
||||
ui::error_with_fix(
|
||||
@@ -5939,13 +5992,7 @@ fn cmd_system_version(json: bool) {
|
||||
}
|
||||
|
||||
fn cmd_reset(confirm: bool) {
|
||||
let openfang_dir = match dirs::home_dir() {
|
||||
Some(h) => h.join(".openfang"),
|
||||
None => {
|
||||
ui::error("Could not determine home directory");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
let openfang_dir = cli_openfang_home();
|
||||
|
||||
if !openfang_dir.exists() {
|
||||
println!(
|
||||
@@ -5980,14 +6027,7 @@ fn cmd_reset(confirm: bool) {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn cmd_uninstall(confirm: bool, keep_config: bool) {
|
||||
let home = match dirs::home_dir() {
|
||||
Some(h) => h,
|
||||
None => {
|
||||
ui::error("Could not determine home directory");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
let openfang_dir = home.join(".openfang");
|
||||
let openfang_dir = cli_openfang_home();
|
||||
let exe_path = std::env::current_exe().ok();
|
||||
|
||||
// Step 1: Show what will be removed
|
||||
@@ -6013,11 +6053,15 @@ fn cmd_uninstall(confirm: bool, keep_config: bool) {
|
||||
println!(" • Remove binary: {}", exe.display());
|
||||
}
|
||||
// Check cargo bin path
|
||||
let cargo_bin = home.join(".cargo").join("bin").join(if cfg!(windows) {
|
||||
"openfang.exe"
|
||||
} else {
|
||||
"openfang"
|
||||
});
|
||||
let cargo_bin = dirs::home_dir()
|
||||
.unwrap_or_else(std::env::temp_dir)
|
||||
.join(".cargo")
|
||||
.join("bin")
|
||||
.join(if cfg!(windows) {
|
||||
"openfang.exe"
|
||||
} else {
|
||||
"openfang"
|
||||
});
|
||||
if cargo_bin.exists() && exe_path.as_ref().is_none_or(|e| *e != cargo_bin) {
|
||||
println!(" • Remove cargo binary: {}", cargo_bin.display());
|
||||
}
|
||||
@@ -6051,12 +6095,13 @@ fn cmd_uninstall(confirm: bool, keep_config: bool) {
|
||||
}
|
||||
|
||||
// Step 4: Remove auto-start entries
|
||||
remove_autostart_entries(&home);
|
||||
let user_home = dirs::home_dir().unwrap_or_else(std::env::temp_dir);
|
||||
remove_autostart_entries(&user_home);
|
||||
|
||||
// Step 5: Clean PATH from shell configs
|
||||
if let Some(ref exe) = exe_path {
|
||||
if let Some(bin_dir) = exe.parent() {
|
||||
clean_path_entries(&home, &bin_dir.to_string_lossy());
|
||||
clean_path_entries(&user_home, &bin_dir.to_string_lossy());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,9 +35,16 @@ pub fn discover_template_dirs() -> Vec<PathBuf> {
|
||||
}
|
||||
}
|
||||
|
||||
// Installed templates
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
let agents = home.join(".openfang").join("agents");
|
||||
// Installed templates (respects OPENFANG_HOME)
|
||||
let of_home = if let Ok(h) = std::env::var("OPENFANG_HOME") {
|
||||
PathBuf::from(h)
|
||||
} else if let Some(home) = dirs::home_dir() {
|
||||
home.join(".openfang")
|
||||
} else {
|
||||
std::env::temp_dir().join(".openfang")
|
||||
};
|
||||
{
|
||||
let agents = of_home.join("agents");
|
||||
if agents.is_dir() && !dirs.contains(&agents) {
|
||||
dirs.push(agents);
|
||||
}
|
||||
|
||||
@@ -825,8 +825,11 @@ fn handle_migration_key(
|
||||
if yes {
|
||||
state.migration_phase = MigrationPhase::Running;
|
||||
let source_dir = state.openclaw_path.clone().unwrap_or_default();
|
||||
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
let target_dir = home.join(".openfang");
|
||||
let target_dir = if let Ok(h) = std::env::var("OPENFANG_HOME") {
|
||||
PathBuf::from(h)
|
||||
} else {
|
||||
dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")).join(".openfang")
|
||||
};
|
||||
let tx = migrate_tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
let options = openfang_migrate::MigrateOptions {
|
||||
@@ -945,15 +948,17 @@ fn save_config(state: &mut State) {
|
||||
}
|
||||
};
|
||||
|
||||
let home = match dirs::home_dir() {
|
||||
Some(h) => h,
|
||||
None => {
|
||||
state.save_error = "Could not determine home directory".to_string();
|
||||
return;
|
||||
let openfang_dir = if let Ok(h) = std::env::var("OPENFANG_HOME") {
|
||||
PathBuf::from(h)
|
||||
} else {
|
||||
match dirs::home_dir() {
|
||||
Some(h) => h.join(".openfang"),
|
||||
None => {
|
||||
state.save_error = "Could not determine home directory".to_string();
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let openfang_dir = home.join(".openfang");
|
||||
let _ = std::fs::create_dir_all(openfang_dir.join("agents"));
|
||||
let _ = std::fs::create_dir_all(openfang_dir.join("data"));
|
||||
crate::restrict_dir_permissions(&openfang_dir);
|
||||
|
||||
@@ -89,11 +89,15 @@ const PROVIDERS: &[ProviderInfo] = &[
|
||||
|
||||
/// Check if first-run setup is needed.
|
||||
pub fn needs_setup() -> bool {
|
||||
let home = match dirs::home_dir() {
|
||||
Some(h) => h,
|
||||
None => return true,
|
||||
let of_home = if let Ok(h) = std::env::var("OPENFANG_HOME") {
|
||||
std::path::PathBuf::from(h)
|
||||
} else {
|
||||
match dirs::home_dir() {
|
||||
Some(h) => h.join(".openfang"),
|
||||
None => return true,
|
||||
}
|
||||
};
|
||||
!home.join(".openfang").join("config.toml").exists()
|
||||
!of_home.join("config.toml").exists()
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
@@ -294,16 +298,18 @@ impl WizardState {
|
||||
}
|
||||
};
|
||||
|
||||
let home = match dirs::home_dir() {
|
||||
Some(h) => h,
|
||||
None => {
|
||||
self.status_msg = "Could not determine home directory".to_string();
|
||||
self.step = WizardStep::Done;
|
||||
return;
|
||||
let openfang_dir = if let Ok(h) = std::env::var("OPENFANG_HOME") {
|
||||
std::path::PathBuf::from(h)
|
||||
} else {
|
||||
match dirs::home_dir() {
|
||||
Some(h) => h.join(".openfang"),
|
||||
None => {
|
||||
self.status_msg = "Could not determine home directory".to_string();
|
||||
self.step = WizardStep::Done;
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let openfang_dir = home.join(".openfang");
|
||||
let _ = std::fs::create_dir_all(openfang_dir.join("agents"));
|
||||
let _ = std::fs::create_dir_all(openfang_dir.join("data"));
|
||||
crate::restrict_dir_permissions(&openfang_dir);
|
||||
|
||||
@@ -38,7 +38,7 @@ pub struct SettingStatus {
|
||||
/// The Hand registry — stores definitions and tracks active instances.
|
||||
pub struct HandRegistry {
|
||||
/// All known hand definitions, keyed by hand_id.
|
||||
definitions: HashMap<String, HandDefinition>,
|
||||
definitions: DashMap<String, HandDefinition>,
|
||||
/// Active hand instances, keyed by instance UUID.
|
||||
instances: DashMap<Uuid, HandInstance>,
|
||||
}
|
||||
@@ -47,13 +47,13 @@ impl HandRegistry {
|
||||
/// Create an empty registry.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
definitions: HashMap::new(),
|
||||
definitions: DashMap::new(),
|
||||
instances: DashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load all bundled hand definitions. Returns count of definitions loaded.
|
||||
pub fn load_bundled(&mut self) -> usize {
|
||||
pub fn load_bundled(&self) -> usize {
|
||||
let bundled = bundled::bundled_hands();
|
||||
let mut count = 0;
|
||||
for (id, toml_content, skill_content) in bundled {
|
||||
@@ -71,16 +71,60 @@ impl HandRegistry {
|
||||
count
|
||||
}
|
||||
|
||||
/// Install a hand from a directory containing HAND.toml (and optional SKILL.md).
|
||||
pub fn install_from_path(&self, path: &std::path::Path) -> HandResult<HandDefinition> {
|
||||
let toml_path = path.join("HAND.toml");
|
||||
let skill_path = path.join("SKILL.md");
|
||||
|
||||
let toml_content = std::fs::read_to_string(&toml_path).map_err(|e| {
|
||||
HandError::NotFound(format!("Cannot read {}: {e}", toml_path.display()))
|
||||
})?;
|
||||
let skill_content = std::fs::read_to_string(&skill_path).unwrap_or_default();
|
||||
|
||||
let def = bundled::parse_bundled("custom", &toml_content, &skill_content)?;
|
||||
|
||||
if self.definitions.contains_key(&def.id) {
|
||||
return Err(HandError::AlreadyActive(format!(
|
||||
"Hand '{}' already registered",
|
||||
def.id
|
||||
)));
|
||||
}
|
||||
|
||||
info!(hand = %def.id, name = %def.name, path = %path.display(), "Installed hand from path");
|
||||
self.definitions.insert(def.id.clone(), def.clone());
|
||||
Ok(def)
|
||||
}
|
||||
|
||||
/// Install a hand from raw TOML + skill content (for API-based installs).
|
||||
pub fn install_from_content(
|
||||
&self,
|
||||
toml_content: &str,
|
||||
skill_content: &str,
|
||||
) -> HandResult<HandDefinition> {
|
||||
let def = bundled::parse_bundled("custom", toml_content, skill_content)?;
|
||||
|
||||
if self.definitions.contains_key(&def.id) {
|
||||
return Err(HandError::AlreadyActive(format!(
|
||||
"Hand '{}' already registered",
|
||||
def.id
|
||||
)));
|
||||
}
|
||||
|
||||
info!(hand = %def.id, name = %def.name, "Installed hand from content");
|
||||
self.definitions.insert(def.id.clone(), def.clone());
|
||||
Ok(def)
|
||||
}
|
||||
|
||||
/// List all known hand definitions.
|
||||
pub fn list_definitions(&self) -> Vec<&HandDefinition> {
|
||||
let mut defs: Vec<&HandDefinition> = self.definitions.values().collect();
|
||||
defs.sort_by_key(|d| &d.name);
|
||||
pub fn list_definitions(&self) -> Vec<HandDefinition> {
|
||||
let mut defs: Vec<HandDefinition> = self.definitions.iter().map(|r| r.value().clone()).collect();
|
||||
defs.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
defs
|
||||
}
|
||||
|
||||
/// Get a specific hand definition by ID.
|
||||
pub fn get_definition(&self, hand_id: &str) -> Option<&HandDefinition> {
|
||||
self.definitions.get(hand_id)
|
||||
pub fn get_definition(&self, hand_id: &str) -> Option<HandDefinition> {
|
||||
self.definitions.get(hand_id).map(|r| r.value().clone())
|
||||
}
|
||||
|
||||
/// Activate a hand — creates an instance (agent spawning is done by kernel).
|
||||
@@ -344,7 +388,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn load_bundled_hands() {
|
||||
let mut reg = HandRegistry::new();
|
||||
let reg = HandRegistry::new();
|
||||
let count = reg.load_bundled();
|
||||
assert_eq!(count, 7);
|
||||
assert!(!reg.list_definitions().is_empty());
|
||||
@@ -368,7 +412,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn activate_and_deactivate() {
|
||||
let mut reg = HandRegistry::new();
|
||||
let reg = HandRegistry::new();
|
||||
reg.load_bundled();
|
||||
|
||||
let instance = reg.activate("clip", HashMap::new()).unwrap();
|
||||
@@ -390,7 +434,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn pause_and_resume() {
|
||||
let mut reg = HandRegistry::new();
|
||||
let reg = HandRegistry::new();
|
||||
reg.load_bundled();
|
||||
|
||||
let instance = reg.activate("clip", HashMap::new()).unwrap();
|
||||
@@ -409,7 +453,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn set_agent() {
|
||||
let mut reg = HandRegistry::new();
|
||||
let reg = HandRegistry::new();
|
||||
reg.load_bundled();
|
||||
|
||||
let instance = reg.activate("clip", HashMap::new()).unwrap();
|
||||
@@ -427,7 +471,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn check_requirements() {
|
||||
let mut reg = HandRegistry::new();
|
||||
let reg = HandRegistry::new();
|
||||
reg.load_bundled();
|
||||
|
||||
let results = reg.check_requirements("clip").unwrap();
|
||||
@@ -452,7 +496,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn set_error_status() {
|
||||
let mut reg = HandRegistry::new();
|
||||
let reg = HandRegistry::new();
|
||||
reg.load_bundled();
|
||||
|
||||
let instance = reg.activate("clip", HashMap::new()).unwrap();
|
||||
|
||||
@@ -224,15 +224,19 @@ pub fn deep_merge_toml(base: &mut toml::Value, overlay: &toml::Value) {
|
||||
}
|
||||
|
||||
/// Get the default config file path.
|
||||
///
|
||||
/// Respects `OPENFANG_HOME` env var (e.g. `OPENFANG_HOME=/opt/openfang`).
|
||||
pub fn default_config_path() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(std::env::temp_dir)
|
||||
.join(".openfang")
|
||||
.join("config.toml")
|
||||
openfang_home().join("config.toml")
|
||||
}
|
||||
|
||||
/// Get the default OpenFang home directory.
|
||||
/// Get the OpenFang home directory.
|
||||
///
|
||||
/// Priority: `OPENFANG_HOME` env var > `~/.openfang`.
|
||||
pub fn openfang_home() -> PathBuf {
|
||||
if let Ok(home) = std::env::var("OPENFANG_HOME") {
|
||||
return PathBuf::from(home);
|
||||
}
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(std::env::temp_dir)
|
||||
.join(".openfang")
|
||||
|
||||
@@ -687,7 +687,7 @@ impl OpenFangKernel {
|
||||
}
|
||||
|
||||
// Initialize hand registry (curated autonomous packages)
|
||||
let mut hand_registry = openfang_hands::registry::HandRegistry::new();
|
||||
let hand_registry = openfang_hands::registry::HandRegistry::new();
|
||||
let hand_count = hand_registry.load_bundled();
|
||||
if hand_count > 0 {
|
||||
info!("Loaded {hand_count} bundled hand(s)");
|
||||
@@ -5031,6 +5031,24 @@ impl KernelHandle for OpenFangKernel {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn hand_install(
|
||||
&self,
|
||||
toml_content: &str,
|
||||
skill_content: &str,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let def = self
|
||||
.hand_registry
|
||||
.install_from_content(toml_content, skill_content)
|
||||
.map_err(|e| format!("{e}"))?;
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"id": def.id,
|
||||
"name": def.name,
|
||||
"description": def.description,
|
||||
"category": format!("{:?}", def.category),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn hand_activate(
|
||||
&self,
|
||||
hand_id: &str,
|
||||
@@ -5057,8 +5075,8 @@ impl KernelHandle for OpenFangKernel {
|
||||
.ok_or_else(|| format!("No active instance found for hand '{hand_id}'"))?;
|
||||
|
||||
let def = self.hand_registry.get_definition(hand_id);
|
||||
let def_name = def.map(|d| d.name.clone()).unwrap_or_default();
|
||||
let def_icon = def.map(|d| d.icon.clone()).unwrap_or_default();
|
||||
let def_name = def.as_ref().map(|d| d.name.clone()).unwrap_or_default();
|
||||
let def_icon = def.as_ref().map(|d| d.icon.clone()).unwrap_or_default();
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"hand_id": hand_id,
|
||||
|
||||
@@ -139,6 +139,16 @@ pub trait KernelHandle: Send + Sync {
|
||||
Err("Hands system not available".to_string())
|
||||
}
|
||||
|
||||
/// Install a Hand from TOML content.
|
||||
async fn hand_install(
|
||||
&self,
|
||||
toml_content: &str,
|
||||
skill_content: &str,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let _ = (toml_content, skill_content);
|
||||
Err("Hands system not available".to_string())
|
||||
}
|
||||
|
||||
/// Activate a Hand — spawns a specialized autonomous agent.
|
||||
async fn hand_activate(
|
||||
&self,
|
||||
|
||||
@@ -627,7 +627,7 @@ pub fn builtin_tool_definitions() -> Vec<ToolDefinition> {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": { "type": "string", "description": "The storage key" },
|
||||
"value": { "description": "The JSON value to store (any type)" }
|
||||
"value": { "type": "string", "description": "The value to store (JSON-encode objects/arrays, or pass a plain string)" }
|
||||
},
|
||||
"required": ["key", "value"]
|
||||
}),
|
||||
@@ -705,7 +705,7 @@ pub fn builtin_tool_definitions() -> Vec<ToolDefinition> {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"event_type": { "type": "string", "description": "Type identifier for the event (e.g., 'code_review_requested')" },
|
||||
"payload": { "description": "JSON payload data for the event" }
|
||||
"payload": { "type": "object", "description": "JSON payload data for the event" }
|
||||
},
|
||||
"required": ["event_type"]
|
||||
}),
|
||||
|
||||
@@ -1168,7 +1168,7 @@ fn default_language() -> String {
|
||||
|
||||
impl Default for KernelConfig {
|
||||
fn default() -> Self {
|
||||
let home_dir = dirs_next_home().join(".openfang");
|
||||
let home_dir = openfang_home_dir();
|
||||
Self {
|
||||
data_dir: home_dir.join("data"),
|
||||
home_dir,
|
||||
@@ -1308,9 +1308,16 @@ impl std::fmt::Debug for KernelConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fallback home directory resolution.
|
||||
fn dirs_next_home() -> PathBuf {
|
||||
dirs::home_dir().unwrap_or_else(std::env::temp_dir)
|
||||
/// Resolve the OpenFang home directory.
|
||||
///
|
||||
/// Priority: `OPENFANG_HOME` env var > `~/.openfang`.
|
||||
fn openfang_home_dir() -> PathBuf {
|
||||
if let Ok(home) = std::env::var("OPENFANG_HOME") {
|
||||
return PathBuf::from(home);
|
||||
}
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(std::env::temp_dir)
|
||||
.join(".openfang")
|
||||
}
|
||||
|
||||
/// Default LLM model configuration.
|
||||
|
||||
Reference in New Issue
Block a user