fix: add missing port/host fields to GatewayConfig and apply_env_overrides method
- Add port and host fields to GatewayConfig struct - Add default_gateway_port() and default_gateway_host() functions - Add apply_env_overrides() method to Config for env var support - Fix test to include new GatewayConfig fields All tests pass.
This commit is contained in:
parent
d7769340a3
commit
a310e178db
16 changed files with 372 additions and 83 deletions
|
|
@ -8,8 +8,8 @@
|
|||
#![allow(clippy::too_many_lines)]
|
||||
#![allow(clippy::unnecessary_map_or)]
|
||||
|
||||
use async_trait::async_trait;
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use lettre::transport::smtp::authentication::Credentials;
|
||||
use lettre::{Message, SmtpTransport, Transport};
|
||||
use mail_parser::{MessageParser, MimeHeaders};
|
||||
|
|
@ -59,11 +59,21 @@ pub struct EmailConfig {
|
|||
pub allowed_senders: Vec<String>,
|
||||
}
|
||||
|
||||
fn default_imap_port() -> u16 { 993 }
|
||||
fn default_smtp_port() -> u16 { 587 }
|
||||
fn default_imap_folder() -> String { "INBOX".into() }
|
||||
fn default_poll_interval() -> u64 { 60 }
|
||||
fn default_true() -> bool { true }
|
||||
fn default_imap_port() -> u16 {
|
||||
993
|
||||
}
|
||||
fn default_smtp_port() -> u16 {
|
||||
587
|
||||
}
|
||||
fn default_imap_folder() -> String {
|
||||
"INBOX".into()
|
||||
}
|
||||
fn default_poll_interval() -> u64 {
|
||||
60
|
||||
}
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl Default for EmailConfig {
|
||||
fn default() -> Self {
|
||||
|
|
@ -137,7 +147,8 @@ impl EmailChannel {
|
|||
|
||||
/// Extract the sender address from a parsed email
|
||||
fn extract_sender(parsed: &mail_parser::Message) -> String {
|
||||
parsed.from()
|
||||
parsed
|
||||
.from()
|
||||
.and_then(|addr| addr.first())
|
||||
.and_then(|a| a.address())
|
||||
.map(|s| s.to_string())
|
||||
|
|
@ -185,13 +196,12 @@ impl EmailChannel {
|
|||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth(),
|
||||
);
|
||||
let server_name: ServerName<'_> =
|
||||
ServerName::try_from(config.imap_host.clone())?;
|
||||
let conn =
|
||||
rustls::ClientConnection::new(tls_config, server_name)?;
|
||||
let server_name: ServerName<'_> = ServerName::try_from(config.imap_host.clone())?;
|
||||
let conn = rustls::ClientConnection::new(tls_config, server_name)?;
|
||||
let mut tls = rustls::StreamOwned::new(conn, tcp);
|
||||
|
||||
let read_line = |tls: &mut rustls::StreamOwned<rustls::ClientConnection, TcpStream>| -> Result<String> {
|
||||
let read_line =
|
||||
|tls: &mut rustls::StreamOwned<rustls::ClientConnection, TcpStream>| -> Result<String> {
|
||||
let mut buf = Vec::new();
|
||||
loop {
|
||||
let mut byte = [0u8; 1];
|
||||
|
|
@ -241,7 +251,11 @@ impl EmailChannel {
|
|||
}
|
||||
|
||||
// Select folder
|
||||
let _select = send_cmd(&mut tls, "A2", &format!("SELECT \"{}\"", config.imap_folder))?;
|
||||
let _select = send_cmd(
|
||||
&mut tls,
|
||||
"A2",
|
||||
&format!("SELECT \"{}\"", config.imap_folder),
|
||||
)?;
|
||||
|
||||
// Search unseen
|
||||
let search_resp = send_cmd(&mut tls, "A3", "SEARCH UNSEEN")?;
|
||||
|
|
@ -285,8 +299,17 @@ impl EmailChannel {
|
|||
.date()
|
||||
.map(|d| {
|
||||
let naive = chrono::NaiveDate::from_ymd_opt(
|
||||
d.year as i32, u32::from(d.month), u32::from(d.day)
|
||||
).and_then(|date| date.and_hms_opt(u32::from(d.hour), u32::from(d.minute), u32::from(d.second)));
|
||||
d.year as i32,
|
||||
u32::from(d.month),
|
||||
u32::from(d.day),
|
||||
)
|
||||
.and_then(|date| {
|
||||
date.and_hms_opt(
|
||||
u32::from(d.hour),
|
||||
u32::from(d.minute),
|
||||
u32::from(d.second),
|
||||
)
|
||||
});
|
||||
naive.map_or(0, |n| n.and_utc().timestamp() as u64)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
|
|
@ -302,7 +325,11 @@ impl EmailChannel {
|
|||
// Mark as seen with unique tag
|
||||
let store_tag = format!("A{tag_counter}");
|
||||
tag_counter += 1;
|
||||
let _ = send_cmd(&mut tls, &store_tag, &format!("STORE {uid} +FLAGS (\\Seen)"));
|
||||
let _ = send_cmd(
|
||||
&mut tls,
|
||||
&store_tag,
|
||||
&format!("STORE {uid} +FLAGS (\\Seen)"),
|
||||
);
|
||||
}
|
||||
|
||||
// Logout with unique tag
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ pub mod imessage;
|
|||
pub mod matrix;
|
||||
pub mod slack;
|
||||
pub mod telegram;
|
||||
pub mod whatsapp;
|
||||
pub mod traits;
|
||||
pub mod whatsapp;
|
||||
|
||||
pub use cli::CliChannel;
|
||||
pub use discord::DiscordChannel;
|
||||
|
|
@ -14,8 +14,8 @@ pub use imessage::IMessageChannel;
|
|||
pub use matrix::MatrixChannel;
|
||||
pub use slack::SlackChannel;
|
||||
pub use telegram::TelegramChannel;
|
||||
pub use whatsapp::WhatsAppChannel;
|
||||
pub use traits::Channel;
|
||||
pub use whatsapp::WhatsAppChannel;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::memory::{self, Memory};
|
||||
|
|
@ -189,7 +189,7 @@ pub fn build_system_prompt(
|
|||
}
|
||||
}
|
||||
|
||||
/// Inject OpenClaw (markdown) identity files into the prompt
|
||||
/// Inject `OpenClaw` (markdown) identity files into the prompt
|
||||
fn inject_openclaw_identity(prompt: &mut String, workspace_dir: &std::path::Path) {
|
||||
#[allow(unused_imports)]
|
||||
use std::fmt::Write;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use super::traits::{Channel, ChannelMessage};
|
|||
use async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// WhatsApp channel — uses WhatsApp Business Cloud API
|
||||
/// `WhatsApp` channel — uses `WhatsApp` Business Cloud API
|
||||
///
|
||||
/// This channel operates in webhook mode (push-based) rather than polling.
|
||||
/// Messages are received via the gateway's `/whatsapp` webhook endpoint.
|
||||
|
|
|
|||
|
|
@ -89,6 +89,12 @@ impl Default for IdentityConfig {
|
|||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GatewayConfig {
|
||||
/// Gateway port (default: 8080)
|
||||
#[serde(default = "default_gateway_port")]
|
||||
pub port: u16,
|
||||
/// Gateway host (default: 127.0.0.1)
|
||||
#[serde(default = "default_gateway_host")]
|
||||
pub host: String,
|
||||
/// Require pairing before accepting requests (default: true)
|
||||
#[serde(default = "default_true")]
|
||||
pub require_pairing: bool,
|
||||
|
|
@ -100,6 +106,14 @@ pub struct GatewayConfig {
|
|||
pub paired_tokens: Vec<String>,
|
||||
}
|
||||
|
||||
fn default_gateway_port() -> u16 {
|
||||
3000
|
||||
}
|
||||
|
||||
fn default_gateway_host() -> String {
|
||||
"127.0.0.1".into()
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
|
@ -107,6 +121,8 @@ fn default_true() -> bool {
|
|||
impl Default for GatewayConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
port: default_gateway_port(),
|
||||
host: default_gateway_host(),
|
||||
require_pairing: true,
|
||||
allow_public_bind: false,
|
||||
paired_tokens: Vec::new(),
|
||||
|
|
@ -649,6 +665,65 @@ impl Config {
|
|||
}
|
||||
}
|
||||
|
||||
/// Apply environment variable overrides to config
|
||||
pub fn apply_env_overrides(&mut self) {
|
||||
// API Key: ZEROCLAW_API_KEY or API_KEY
|
||||
if let Ok(key) = std::env::var("ZEROCLAW_API_KEY").or_else(|_| std::env::var("API_KEY")) {
|
||||
if !key.is_empty() {
|
||||
self.api_key = Some(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Provider: ZEROCLAW_PROVIDER or PROVIDER
|
||||
if let Ok(provider) =
|
||||
std::env::var("ZEROCLAW_PROVIDER").or_else(|_| std::env::var("PROVIDER"))
|
||||
{
|
||||
if !provider.is_empty() {
|
||||
self.default_provider = Some(provider);
|
||||
}
|
||||
}
|
||||
|
||||
// Model: ZEROCLAW_MODEL
|
||||
if let Ok(model) = std::env::var("ZEROCLAW_MODEL") {
|
||||
if !model.is_empty() {
|
||||
self.default_model = Some(model);
|
||||
}
|
||||
}
|
||||
|
||||
// Workspace directory: ZEROCLAW_WORKSPACE
|
||||
if let Ok(workspace) = std::env::var("ZEROCLAW_WORKSPACE") {
|
||||
if !workspace.is_empty() {
|
||||
self.workspace_dir = PathBuf::from(workspace);
|
||||
}
|
||||
}
|
||||
|
||||
// Gateway port: ZEROCLAW_GATEWAY_PORT or PORT
|
||||
if let Ok(port_str) =
|
||||
std::env::var("ZEROCLAW_GATEWAY_PORT").or_else(|_| std::env::var("PORT"))
|
||||
{
|
||||
if let Ok(port) = port_str.parse::<u16>() {
|
||||
self.gateway.port = port;
|
||||
}
|
||||
}
|
||||
|
||||
// Gateway host: ZEROCLAW_GATEWAY_HOST or HOST
|
||||
if let Ok(host) = std::env::var("ZEROCLAW_GATEWAY_HOST").or_else(|_| std::env::var("HOST"))
|
||||
{
|
||||
if !host.is_empty() {
|
||||
self.gateway.host = host;
|
||||
}
|
||||
}
|
||||
|
||||
// Temperature: ZEROCLAW_TEMPERATURE
|
||||
if let Ok(temp_str) = std::env::var("ZEROCLAW_TEMPERATURE") {
|
||||
if let Ok(temp) = temp_str.parse::<f64>() {
|
||||
if (0.0..=2.0).contains(&temp) {
|
||||
self.default_temperature = temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save(&self) -> Result<()> {
|
||||
let toml_str = toml::to_string_pretty(self).context("Failed to serialize config")?;
|
||||
fs::write(&self.config_path, toml_str).context("Failed to write config file")?;
|
||||
|
|
@ -1191,6 +1266,8 @@ channel_id = "C123"
|
|||
#[test]
|
||||
fn checklist_gateway_serde_roundtrip() {
|
||||
let g = GatewayConfig {
|
||||
port: 3000,
|
||||
host: "127.0.0.1".into(),
|
||||
require_pairing: true,
|
||||
allow_public_bind: false,
|
||||
paired_tokens: vec!["zc_test_token".into()],
|
||||
|
|
@ -1364,4 +1441,187 @@ default_temperature = 0.7
|
|||
assert!(!parsed.browser.enabled);
|
||||
assert!(parsed.browser.allowed_domains.is_empty());
|
||||
}
|
||||
|
||||
// ── Environment variable overrides (Docker support) ─────────
|
||||
|
||||
#[test]
|
||||
fn env_override_api_key() {
|
||||
let mut config = Config::default();
|
||||
assert!(config.api_key.is_none());
|
||||
|
||||
std::env::set_var("ZEROCLAW_API_KEY", "sk-test-env-key");
|
||||
config.apply_env_overrides();
|
||||
assert_eq!(config.api_key.as_deref(), Some("sk-test-env-key"));
|
||||
|
||||
std::env::remove_var("ZEROCLAW_API_KEY");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_override_api_key_fallback() {
|
||||
let mut config = Config::default();
|
||||
|
||||
std::env::remove_var("ZEROCLAW_API_KEY");
|
||||
std::env::set_var("API_KEY", "sk-fallback-key");
|
||||
config.apply_env_overrides();
|
||||
assert_eq!(config.api_key.as_deref(), Some("sk-fallback-key"));
|
||||
|
||||
std::env::remove_var("API_KEY");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_override_provider() {
|
||||
let mut config = Config::default();
|
||||
|
||||
std::env::set_var("ZEROCLAW_PROVIDER", "anthropic");
|
||||
config.apply_env_overrides();
|
||||
assert_eq!(config.default_provider.as_deref(), Some("anthropic"));
|
||||
|
||||
std::env::remove_var("ZEROCLAW_PROVIDER");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_override_provider_fallback() {
|
||||
let mut config = Config::default();
|
||||
|
||||
std::env::remove_var("ZEROCLAW_PROVIDER");
|
||||
std::env::set_var("PROVIDER", "openai");
|
||||
config.apply_env_overrides();
|
||||
assert_eq!(config.default_provider.as_deref(), Some("openai"));
|
||||
|
||||
std::env::remove_var("PROVIDER");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_override_model() {
|
||||
let mut config = Config::default();
|
||||
|
||||
std::env::set_var("ZEROCLAW_MODEL", "gpt-4o");
|
||||
config.apply_env_overrides();
|
||||
assert_eq!(config.default_model.as_deref(), Some("gpt-4o"));
|
||||
|
||||
std::env::remove_var("ZEROCLAW_MODEL");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_override_workspace() {
|
||||
let mut config = Config::default();
|
||||
|
||||
std::env::set_var("ZEROCLAW_WORKSPACE", "/custom/workspace");
|
||||
config.apply_env_overrides();
|
||||
assert_eq!(config.workspace_dir, PathBuf::from("/custom/workspace"));
|
||||
|
||||
std::env::remove_var("ZEROCLAW_WORKSPACE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_override_empty_values_ignored() {
|
||||
let mut config = Config::default();
|
||||
let original_provider = config.default_provider.clone();
|
||||
|
||||
std::env::set_var("ZEROCLAW_PROVIDER", "");
|
||||
config.apply_env_overrides();
|
||||
assert_eq!(config.default_provider, original_provider);
|
||||
|
||||
std::env::remove_var("ZEROCLAW_PROVIDER");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_override_gateway_port() {
|
||||
let mut config = Config::default();
|
||||
assert_eq!(config.gateway.port, 3000);
|
||||
|
||||
std::env::set_var("ZEROCLAW_GATEWAY_PORT", "8080");
|
||||
config.apply_env_overrides();
|
||||
assert_eq!(config.gateway.port, 8080);
|
||||
|
||||
std::env::remove_var("ZEROCLAW_GATEWAY_PORT");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_override_port_fallback() {
|
||||
let mut config = Config::default();
|
||||
|
||||
std::env::remove_var("ZEROCLAW_GATEWAY_PORT");
|
||||
std::env::set_var("PORT", "9000");
|
||||
config.apply_env_overrides();
|
||||
assert_eq!(config.gateway.port, 9000);
|
||||
|
||||
std::env::remove_var("PORT");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_override_gateway_host() {
|
||||
let mut config = Config::default();
|
||||
assert_eq!(config.gateway.host, "127.0.0.1");
|
||||
|
||||
std::env::set_var("ZEROCLAW_GATEWAY_HOST", "0.0.0.0");
|
||||
config.apply_env_overrides();
|
||||
assert_eq!(config.gateway.host, "0.0.0.0");
|
||||
|
||||
std::env::remove_var("ZEROCLAW_GATEWAY_HOST");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_override_host_fallback() {
|
||||
let mut config = Config::default();
|
||||
|
||||
std::env::remove_var("ZEROCLAW_GATEWAY_HOST");
|
||||
std::env::set_var("HOST", "0.0.0.0");
|
||||
config.apply_env_overrides();
|
||||
assert_eq!(config.gateway.host, "0.0.0.0");
|
||||
|
||||
std::env::remove_var("HOST");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_override_temperature() {
|
||||
let mut config = Config::default();
|
||||
|
||||
std::env::set_var("ZEROCLAW_TEMPERATURE", "0.5");
|
||||
config.apply_env_overrides();
|
||||
assert!((config.default_temperature - 0.5).abs() < f64::EPSILON);
|
||||
|
||||
std::env::remove_var("ZEROCLAW_TEMPERATURE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_override_temperature_out_of_range_ignored() {
|
||||
// Clean up any leftover env vars from other tests
|
||||
std::env::remove_var("ZEROCLAW_TEMPERATURE");
|
||||
|
||||
let mut config = Config::default();
|
||||
let original_temp = config.default_temperature;
|
||||
|
||||
// Temperature > 2.0 should be ignored
|
||||
std::env::set_var("ZEROCLAW_TEMPERATURE", "3.0");
|
||||
config.apply_env_overrides();
|
||||
assert!(
|
||||
(config.default_temperature - original_temp).abs() < f64::EPSILON,
|
||||
"Temperature 3.0 should be ignored (out of range)"
|
||||
);
|
||||
|
||||
std::env::remove_var("ZEROCLAW_TEMPERATURE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_override_invalid_port_ignored() {
|
||||
let mut config = Config::default();
|
||||
let original_port = config.gateway.port;
|
||||
|
||||
std::env::set_var("PORT", "not_a_number");
|
||||
config.apply_env_overrides();
|
||||
assert_eq!(config.gateway.port, original_port);
|
||||
|
||||
std::env::remove_var("PORT");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_config_default_values() {
|
||||
let g = GatewayConfig::default();
|
||||
assert_eq!(g.port, 3000);
|
||||
assert_eq!(g.host, "127.0.0.1");
|
||||
assert!(g.require_pairing);
|
||||
assert!(!g.allow_public_bind);
|
||||
assert!(g.paired_tokens.is_empty());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ pub struct CronJob {
|
|||
pub last_status: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn handle_command(command: super::CronCommands, config: Config) -> Result<()> {
|
||||
match command {
|
||||
super::CronCommands::List => {
|
||||
|
|
@ -33,8 +34,7 @@ pub fn handle_command(command: super::CronCommands, config: Config) -> Result<()
|
|||
for job in jobs {
|
||||
let last_run = job
|
||||
.last_run
|
||||
.map(|d| d.to_rfc3339())
|
||||
.unwrap_or_else(|| "never".into());
|
||||
.map_or_else(|| "never".into(), |d| d.to_rfc3339());
|
||||
let last_status = job.last_status.unwrap_or_else(|| "n/a".into());
|
||||
println!(
|
||||
"- {} | {} | next={} | last={} ({})\n cmd: {}",
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ async fn execute_job_with_retry(
|
|||
}
|
||||
|
||||
if attempt < retries {
|
||||
let jitter_ms = (Utc::now().timestamp_subsec_millis() % 250) as u64;
|
||||
let jitter_ms = u64::from(Utc::now().timestamp_subsec_millis() % 250);
|
||||
time::sleep(Duration::from_millis(backoff_ms + jitter_ms)).await;
|
||||
backoff_ms = (backoff_ms.saturating_mul(2)).min(30_000);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,25 +52,21 @@ pub fn run(config: &Config) -> Result<()> {
|
|||
let scheduler_ok = scheduler
|
||||
.get("status")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(|s| s == "ok")
|
||||
.unwrap_or(false);
|
||||
.is_some_and(|s| s == "ok");
|
||||
|
||||
let scheduler_last_ok = scheduler
|
||||
.get("last_ok")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.and_then(parse_rfc3339)
|
||||
.map(|dt| Utc::now().signed_duration_since(dt).num_seconds())
|
||||
.unwrap_or(i64::MAX);
|
||||
.map_or(i64::MAX, |dt| {
|
||||
Utc::now().signed_duration_since(dt).num_seconds()
|
||||
});
|
||||
|
||||
if scheduler_ok && scheduler_last_ok <= SCHEDULER_STALE_SECONDS {
|
||||
println!(
|
||||
" ✅ scheduler healthy (last ok {}s ago)",
|
||||
scheduler_last_ok
|
||||
);
|
||||
println!(" ✅ scheduler healthy (last ok {scheduler_last_ok}s ago)");
|
||||
} else {
|
||||
println!(
|
||||
" ❌ scheduler unhealthy/stale (status_ok={}, age={}s)",
|
||||
scheduler_ok, scheduler_last_ok
|
||||
" ❌ scheduler unhealthy/stale (status_ok={scheduler_ok}, age={scheduler_last_ok}s)"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -86,14 +82,14 @@ pub fn run(config: &Config) -> Result<()> {
|
|||
let status_ok = component
|
||||
.get("status")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(|s| s == "ok")
|
||||
.unwrap_or(false);
|
||||
.is_some_and(|s| s == "ok");
|
||||
let age = component
|
||||
.get("last_ok")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.and_then(parse_rfc3339)
|
||||
.map(|dt| Utc::now().signed_duration_since(dt).num_seconds())
|
||||
.unwrap_or(i64::MAX);
|
||||
.map_or(i64::MAX, |dt| {
|
||||
Utc::now().signed_duration_since(dt).num_seconds()
|
||||
});
|
||||
|
||||
if status_ok && age <= CHANNEL_STALE_SECONDS {
|
||||
println!(" ✅ {name} fresh (last ok {age}s ago)");
|
||||
|
|
@ -107,10 +103,7 @@ pub fn run(config: &Config) -> Result<()> {
|
|||
if channel_count == 0 {
|
||||
println!(" ℹ️ no channel components tracked in state yet");
|
||||
} else {
|
||||
println!(
|
||||
" Channel summary: {} total, {} stale",
|
||||
channel_count, stale_channels
|
||||
);
|
||||
println!(" Channel summary: {channel_count} total, {stale_channels} stale");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ pub fn mark_component_ok(component: &str) {
|
|||
});
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn mark_component_error(component: &str, error: impl ToString) {
|
||||
let err = error.to_string();
|
||||
upsert_component(component, move |entry| {
|
||||
|
|
|
|||
|
|
@ -169,9 +169,9 @@ enum Commands {
|
|||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum MigrateCommands {
|
||||
/// Import memory from an OpenClaw workspace into this ZeroClaw workspace
|
||||
/// Import memory from an `OpenClaw` workspace into this `ZeroClaw` workspace
|
||||
Openclaw {
|
||||
/// Optional path to OpenClaw workspace (defaults to ~/.openclaw/workspace)
|
||||
/// Optional path to `OpenClaw` workspace (defaults to ~/.openclaw/workspace)
|
||||
#[arg(long)]
|
||||
source: Option<std::path::PathBuf>,
|
||||
|
||||
|
|
|
|||
|
|
@ -250,6 +250,7 @@ fn read_openclaw_markdown_entries(source_workspace: &Path) -> Result<Vec<SourceE
|
|||
Ok(all)
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn parse_markdown_file(
|
||||
_path: &Path,
|
||||
content: &str,
|
||||
|
|
@ -306,10 +307,9 @@ fn parse_structured_memory_line(line: &str) -> Option<(&str, &str)> {
|
|||
|
||||
fn parse_category(raw: &str) -> MemoryCategory {
|
||||
match raw.trim().to_ascii_lowercase().as_str() {
|
||||
"core" => MemoryCategory::Core,
|
||||
"core" | "" => MemoryCategory::Core,
|
||||
"daily" => MemoryCategory::Daily,
|
||||
"conversation" => MemoryCategory::Conversation,
|
||||
"" => MemoryCategory::Core,
|
||||
other => MemoryCategory::Custom(other.to_string()),
|
||||
}
|
||||
}
|
||||
|
|
@ -350,7 +350,7 @@ fn pick_optional_column_expr(columns: &[String], candidates: &[&str]) -> Option<
|
|||
candidates
|
||||
.iter()
|
||||
.find(|candidate| columns.iter().any(|c| c == *candidate))
|
||||
.map(|s| s.to_string())
|
||||
.map(std::string::ToString::to_string)
|
||||
}
|
||||
|
||||
fn pick_column_expr(columns: &[String], candidates: &[&str], fallback: &str) -> String {
|
||||
|
|
|
|||
|
|
@ -451,7 +451,10 @@ fn setup_provider() -> Result<(String, String, String)> {
|
|||
("mistral", "Mistral — Large & Codestral"),
|
||||
("xai", "xAI — Grok 3 & 4"),
|
||||
("perplexity", "Perplexity — search-augmented AI"),
|
||||
("gemini", "Google Gemini — Gemini 2.0 Flash & Pro (supports CLI auth)"),
|
||||
(
|
||||
"gemini",
|
||||
"Google Gemini — Gemini 2.0 Flash & Pro (supports CLI auth)",
|
||||
),
|
||||
],
|
||||
1 => vec![
|
||||
("groq", "Groq — ultra-fast LPU inference"),
|
||||
|
|
@ -534,7 +537,10 @@ fn setup_provider() -> Result<(String, String, String)> {
|
|||
let api_key = if provider_name == "ollama" {
|
||||
print_bullet("Ollama runs locally — no API key needed!");
|
||||
String::new()
|
||||
} else if provider_name == "gemini" || provider_name == "google" || provider_name == "google-gemini" {
|
||||
} else if provider_name == "gemini"
|
||||
|| provider_name == "google"
|
||||
|| provider_name == "google-gemini"
|
||||
{
|
||||
// Special handling for Gemini: check for CLI auth first
|
||||
if crate::providers::gemini::GeminiProvider::has_cli_credentials() {
|
||||
print_bullet(&format!(
|
||||
|
|
@ -741,7 +747,10 @@ fn setup_provider() -> Result<(String, String, String)> {
|
|||
],
|
||||
"gemini" | "google" | "google-gemini" => vec![
|
||||
("gemini-2.0-flash", "Gemini 2.0 Flash (fast, recommended)"),
|
||||
("gemini-2.0-flash-lite", "Gemini 2.0 Flash Lite (fastest, cheapest)"),
|
||||
(
|
||||
"gemini-2.0-flash-lite",
|
||||
"Gemini 2.0 Flash Lite (fastest, cheapest)",
|
||||
),
|
||||
("gemini-1.5-pro", "Gemini 1.5 Pro (best quality)"),
|
||||
("gemini-1.5-flash", "Gemini 1.5 Flash (balanced)"),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -194,7 +194,10 @@ impl SecretStore {
|
|||
let _ = std::process::Command::new("icacls")
|
||||
.arg(&self.key_path)
|
||||
.args(["/inheritance:r", "/grant:r"])
|
||||
.arg(format!("{}:F", std::env::var("USERNAME").unwrap_or_default()))
|
||||
.arg(format!(
|
||||
"{}:F",
|
||||
std::env::var("USERNAME").unwrap_or_default()
|
||||
))
|
||||
.output();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use std::process::Command;
|
|||
|
||||
const SERVICE_LABEL: &str = "com.zeroclaw.daemon";
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn handle_command(command: super::ServiceCommands, config: &Config) -> Result<()> {
|
||||
match command {
|
||||
super::ServiceCommands::Install => install(config),
|
||||
|
|
|
|||
|
|
@ -239,6 +239,7 @@ fn copy_dir_recursive(src: &Path, dest: &Path) -> Result<()> {
|
|||
}
|
||||
|
||||
/// Handle the `skills` CLI command
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn handle_command(command: super::SkillCommands, workspace_dir: &Path) -> Result<()> {
|
||||
match command {
|
||||
super::SkillCommands::List => {
|
||||
|
|
|
|||
|
|
@ -69,15 +69,12 @@ impl Tool for FileWriteTool {
|
|||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
let parent = match full_path.parent() {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
let Some(parent) = full_path.parent() else {
|
||||
return Ok(ToolResult {
|
||||
success: false,
|
||||
output: String::new(),
|
||||
error: Some("Invalid path: missing parent directory".into()),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Resolve parent before writing to block symlink escapes.
|
||||
|
|
@ -103,15 +100,12 @@ impl Tool for FileWriteTool {
|
|||
});
|
||||
}
|
||||
|
||||
let file_name = match full_path.file_name() {
|
||||
Some(name) => name,
|
||||
None => {
|
||||
let Some(file_name) = full_path.file_name() else {
|
||||
return Ok(ToolResult {
|
||||
success: false,
|
||||
output: String::new(),
|
||||
error: Some("Invalid path: missing file name".into()),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let resolved_target = resolved_parent.join(file_name);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue