use std::fs;
use std::path::Path;
use super::{
AreaResult, LegacyConfig, MigrationArea, err, qt, read_from_keystore, store_in_keystore,
};
pub(crate) fn import_config(oc_root: &Path, ic_root: &Path) -> AreaResult {
let ks_path = ic_root.join("keystore.enc");
let config_path = oc_root.join("legacy.json");
if !config_path.exists() {
return err(
MigrationArea::Config,
format!("legacy.json not found at {}", config_path.display()),
);
}
let content = match fs::read_to_string(&config_path) {
Ok(c) => c,
Err(e) => {
return err(
MigrationArea::Config,
format!("Failed to read legacy.json: {e}"),
);
}
};
let oc_cfg: LegacyConfig = match serde_json::from_str(&content) {
Ok(c) => c,
Err(e) => {
return err(
MigrationArea::Config,
format!("Failed to parse legacy.json: {e}"),
);
}
};
let mut warnings = Vec::new();
let mut toml = Vec::new();
let agents_defaults = oc_cfg.extra.get("agents").and_then(|a| a.get("defaults"));
let gateway = oc_cfg.extra.get("gateway");
toml.push("[agent]".into());
let soul_name = {
let soul_path = oc_root.join("workspace").join("SOUL.md");
if soul_path.exists() {
fs::read_to_string(&soul_path).ok().and_then(|s| {
s.lines().find_map(|l| {
let trimmed = l.trim();
if let Some(rest) = trimmed.strip_prefix("I am ") {
let name = rest
.split([',', '.', '\u{2014}', '-'])
.next()
.unwrap_or(rest)
.trim();
if !name.is_empty() {
return Some(name.to_string());
}
}
None
})
})
} else {
None
}
};
let name = oc_cfg
.name
.as_deref()
.or(soul_name.as_deref())
.unwrap_or("Migrated Agent");
let id = name.to_lowercase().replace(' ', "-");
toml.push(format!("name = {}", qt(name)));
toml.push(format!("id = {}", qt(&id)));
toml.push(format!(
"workspace = {}",
qt(&ic_root.join("workspace").to_string_lossy())
));
toml.push(String::new());
toml.push("[server]".into());
let bind = gateway
.and_then(|g| g.get("bind"))
.and_then(|v| v.as_str())
.unwrap_or("loopback");
let host = if bind == "loopback" {
"localhost"
} else {
"0.0.0.0"
};
let port = gateway
.and_then(|g| g.get("port"))
.and_then(|v| v.as_u64())
.unwrap_or(18789);
toml.push(format!("host = {}", qt(host)));
toml.push(format!("port = {port}"));
if let Some(token) = gateway
.and_then(|g| g.get("auth"))
.and_then(|a| a.get("token"))
.and_then(|v| v.as_str())
&& !token.is_empty()
{
match store_in_keystore("gateway_auth_token", token, &ks_path) {
Ok(()) => {
toml.push(format!(
"api_key_ref = {}",
qt("keystore:gateway_auth_token")
));
warnings
.push("Gateway auth token stored in keystore as \"gateway_auth_token\"".into());
}
Err(e) => {
toml.push(format!("api_key_env = {}", qt("ROBOTICUS_API_KEY")));
warnings.push(format!(
"Keystore unavailable ({e}); set ROBOTICUS_API_KEY to your gateway token"
));
}
}
}
toml.push(String::new());
toml.push("[database]".into());
toml.push(format!(
"path = {}",
qt(&ic_root.join("state.db").to_string_lossy())
));
toml.push(String::new());
let oc_providers: Vec<(String, serde_json::Value)> = oc_cfg
.extra
.get("models")
.and_then(|m| m.get("providers"))
.and_then(|p| p.as_object())
.map(|obj| obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
.unwrap_or_default();
toml.push("[models]".into());
let agent_primary = agents_defaults
.and_then(|d| d.get("model"))
.and_then(|m| m.get("primary"))
.and_then(|v| v.as_str());
let agent_fallbacks: Vec<String> = agents_defaults
.and_then(|d| d.get("model"))
.and_then(|m| m.get("fallbacks"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
if let Some(primary) = agent_primary {
toml.push(format!("primary = {}", qt(primary)));
if !agent_fallbacks.is_empty() {
let refs: Vec<String> = agent_fallbacks.iter().map(|r| qt(r)).collect();
toml.push(format!("fallbacks = [{}]", refs.join(", ")));
}
} else if !oc_providers.is_empty() {
let mut all_model_refs: Vec<String> = Vec::new();
let mut primary_set = false;
for (prov_name, prov) in &oc_providers {
if let Some(models) = prov.get("models").and_then(|m| m.as_array()) {
for model in models {
if let Some(id) = model.get("id").and_then(|v| v.as_str()) {
let model_ref = format!("{prov_name}/{id}");
if !primary_set {
toml.push(format!("primary = {}", qt(&model_ref)));
primary_set = true;
} else {
all_model_refs.push(model_ref);
}
}
}
}
}
if !primary_set {
if let Some(model) = &oc_cfg.model {
toml.push(format!("primary = {}", qt(model)));
} else {
toml.push("primary = \"gpt-4\"".into());
warnings.push("No model specified in Legacy config, defaulting to gpt-4".into());
}
}
if !all_model_refs.is_empty() {
let refs: Vec<String> = all_model_refs.iter().map(|r| qt(r)).collect();
toml.push(format!("fallbacks = [{}]", refs.join(", ")));
}
} else if let Some(model) = &oc_cfg.model {
toml.push(format!("primary = {}", qt(model)));
} else {
toml.push("primary = \"gpt-4\"".into());
warnings.push("No model specified in Legacy config, defaulting to gpt-4".into());
}
if let Some(temp) = oc_cfg.temperature {
toml.push(format!("temperature = {temp}"));
}
if let Some(max) = oc_cfg.max_tokens {
toml.push(format!("max_tokens = {max}"));
}
toml.push(String::new());
if !oc_providers.is_empty() {
for (prov_name, prov) in &oc_providers {
toml.push(format!("[providers.{prov_name}]"));
if let Some(url) = prov.get("baseUrl").and_then(|v| v.as_str()) {
toml.push(format!("url = {}", qt(url)));
}
let oc_api = prov
.get("api")
.and_then(|v| v.as_str())
.unwrap_or("openai-completions");
let (format_str, chat_path, auth_header) = match oc_api {
"anthropic-messages" => ("anthropic", "/v1/messages", "x-api-key"),
"google-generative-ai" => (
"google",
"/models/gemini-2.0-flash:generateContent",
"query:key",
),
_ => ("openai", "/v1/chat/completions", "Authorization"),
};
toml.push(format!("chat_path = {}", qt(chat_path)));
toml.push(format!("format = {}", qt(format_str)));
toml.push(format!("auth_header = {}", qt(auth_header)));
let is_local = prov_name.starts_with("ollama");
let first_cost = prov
.get("models")
.and_then(|m| m.as_array())
.and_then(|arr| arr.first())
.and_then(|m| m.get("cost"))
.and_then(|c| c.get("input"))
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
let tier = if is_local {
"T1"
} else if first_cost <= 1.0 {
"T2"
} else {
"T3"
};
toml.push(format!("tier = {}", qt(tier)));
if is_local {
toml.push("is_local = true".into());
}
let cost_in = prov
.get("models")
.and_then(|m| m.as_array())
.and_then(|arr| arr.first())
.and_then(|m| m.get("cost"))
.and_then(|c| c.get("input"))
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
let cost_out = prov
.get("models")
.and_then(|m| m.as_array())
.and_then(|arr| arr.first())
.and_then(|m| m.get("cost"))
.and_then(|c| c.get("output"))
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
toml.push(format!("cost_per_input_token = {}", cost_in / 1_000_000.0));
toml.push(format!(
"cost_per_output_token = {}",
cost_out / 1_000_000.0
));
if let Some(api_key) = prov.get("apiKey").and_then(|v| v.as_str())
&& !api_key.is_empty()
&& api_key != "ollama"
&& api_key != "ollama-local"
&& api_key != "not-needed"
{
let ks_name = format!("{prov_name}_api_key");
match store_in_keystore(&ks_name, api_key, &ks_path) {
Ok(()) => {
toml.push(format!(
"api_key_ref = {}",
qt(&format!("keystore:{ks_name}"))
));
warnings.push(format!(
"{prov_name} API key stored in encrypted keystore as \"{ks_name}\""
));
}
Err(e) => {
let env_name =
format!("{}_API_KEY", prov_name.to_uppercase().replace('-', "_"));
toml.push(format!("api_key_env = {}", qt(&env_name)));
warnings.push(format!(
"Keystore unavailable ({e}); set env var {env_name}=<your-key>"
));
}
}
}
toml.push(String::new());
}
} else if let Some(provider) = &oc_cfg.provider {
let key = provider.to_lowercase();
toml.push(format!("[providers.{key}]"));
if let Some(url) = &oc_cfg.api_url {
toml.push(format!("url = {}", qt(url)));
}
if let Some(api_key) = &oc_cfg.api_key {
let ks_name = format!("{}_api_key", key);
match store_in_keystore(&ks_name, api_key, &ks_path) {
Ok(()) => {
toml.push(format!(
"api_key_ref = {}",
qt(&format!("keystore:{ks_name}"))
));
warnings.push(format!(
"API key stored in encrypted keystore as \"{ks_name}\""
));
}
Err(e) => {
let env_name = format!("{}_API_KEY", provider.to_uppercase());
toml.push(format!("api_key_env = {}", qt(&env_name)));
warnings.push(format!(
"Keystore unavailable ({e}); set env var {env_name}=<your-key>"
));
}
}
}
toml.push(String::new());
}
if let Some(channels) = &oc_cfg.channels {
if let Some(tg) = &channels.telegram {
toml.push("[channels.telegram]".into());
toml.push(format!("enabled = {}", tg.enabled.unwrap_or(false)));
if let Some(token) = &tg.token {
match store_in_keystore("telegram_bot_token", token, &ks_path) {
Ok(()) => {
toml.push("token_ref = \"keystore:telegram_bot_token\"".into());
warnings.push(
"Telegram token stored in encrypted keystore as \"telegram_bot_token\""
.into(),
);
}
Err(e) => {
toml.push("token_env = \"TELEGRAM_BOT_TOKEN\"".into());
warnings.push(format!(
"Keystore unavailable ({e}); set env var TELEGRAM_BOT_TOKEN=<token>"
));
}
}
}
toml.push("poll_timeout_seconds = 30".into());
toml.push("allowed_chat_ids = []".into());
toml.push("webhook_mode = false".into());
toml.push(String::new());
}
if let Some(wa) = &channels.whatsapp {
toml.push("[channels.whatsapp]".into());
toml.push(format!("enabled = {}", wa.enabled.unwrap_or(false)));
if let Some(token) = &wa.token {
match store_in_keystore("whatsapp_token", token, &ks_path) {
Ok(()) => {
toml.push("token_ref = \"keystore:whatsapp_token\"".into());
warnings.push(
"WhatsApp token stored in encrypted keystore as \"whatsapp_token\""
.into(),
);
}
Err(e) => {
toml.push("token_env = \"WHATSAPP_TOKEN\"".into());
warnings.push(format!(
"Keystore unavailable ({e}); set env var WHATSAPP_TOKEN=<token>"
));
}
}
}
if let Some(phone) = &wa.phone_id {
toml.push(format!("phone_number_id = {}", qt(phone)));
}
toml.push(String::new());
}
}
if let Err(e) = fs::create_dir_all(ic_root) {
return err(
MigrationArea::Config,
format!("Failed to create output dir: {e}"),
);
}
if let Err(e) = fs::write(ic_root.join("roboticus.toml"), toml.join("\n")) {
return err(
MigrationArea::Config,
format!("Failed to write roboticus.toml: {e}"),
);
}
AreaResult {
area: MigrationArea::Config,
success: true,
items_processed: 1,
warnings,
error: None,
}
}
pub(crate) fn export_config(ic_root: &Path, oc_root: &Path) -> AreaResult {
let ks_path = ic_root.join("keystore.enc");
let config_path = ic_root.join("roboticus.toml");
if !config_path.exists() {
return err(MigrationArea::Config, "roboticus.toml not found".into());
}
let content = match fs::read_to_string(&config_path) {
Ok(c) => c,
Err(e) => {
return err(
MigrationArea::Config,
format!("Failed to read roboticus.toml: {e}"),
);
}
};
let tv: toml::Value = match toml::from_str(&content) {
Ok(v) => v,
Err(e) => {
return err(
MigrationArea::Config,
format!("Failed to parse roboticus.toml: {e}"),
);
}
};
let mut oc = serde_json::Map::new();
let mut warnings = Vec::new();
if let Some(name) = tv
.get("agent")
.and_then(|a| a.get("name"))
.and_then(|v| v.as_str())
{
oc.insert("name".into(), serde_json::Value::String(name.into()));
}
if let Some(models) = tv.get("models").and_then(|v| v.as_table()) {
if let Some(p) = models.get("primary").and_then(|v| v.as_str()) {
oc.insert("model".into(), serde_json::Value::String(p.into()));
}
if let Some(t) = models.get("temperature").and_then(|v| v.as_float()) {
oc.insert("temperature".into(), serde_json::json!(t));
}
if let Some(m) = models.get("max_tokens").and_then(|v| v.as_integer()) {
oc.insert("max_tokens".into(), serde_json::json!(m));
}
}
if let Some(providers) = tv.get("providers").and_then(|v| v.as_table())
&& let Some((name, prov)) = providers.iter().next()
{
oc.insert("provider".into(), serde_json::Value::String(name.clone()));
if let Some(url) = prov.get("base_url").and_then(|v| v.as_str()) {
oc.insert("api_url".into(), serde_json::Value::String(url.into()));
}
let mut key_resolved = false;
if let Some(key_ref) = prov.get("api_key_ref").and_then(|v| v.as_str())
&& let Some(ks_name) = key_ref.strip_prefix("keystore:")
{
if let Some(val) = read_from_keystore(ks_name, &ks_path) {
oc.insert("api_key".into(), serde_json::Value::String(val));
key_resolved = true;
} else {
warnings.push(format!(
"Keystore key \"{ks_name}\" not found; api_key omitted"
));
}
}
if !key_resolved && let Some(key_env) = prov.get("api_key_env").and_then(|v| v.as_str()) {
if let Ok(val) = std::env::var(key_env) {
oc.insert("api_key".into(), serde_json::Value::String(val));
} else {
warnings.push(format!("Env var {key_env} not set; api_key omitted"));
}
}
}
let oc_config_path = oc_root.join("legacy.json");
let mut merged: serde_json::Map<String, serde_json::Value> = if oc_config_path.exists() {
fs::read_to_string(&oc_config_path)
.inspect_err(|e| tracing::warn!(path = %oc_config_path.display(), "failed to read legacy.json: {e}"))
.ok()
.and_then(|c| serde_json::from_str(&c)
.inspect_err(|e| tracing::warn!(path = %oc_config_path.display(), "failed to parse legacy.json: {e}"))
.ok())
.unwrap_or_default()
} else {
serde_json::Map::new()
};
for (k, v) in oc {
merged.insert(k, v);
}
if let Err(e) = fs::create_dir_all(oc_root) {
return err(
MigrationArea::Config,
format!("Failed to create output dir: {e}"),
);
}
let json = match serde_json::to_string_pretty(&merged) {
Ok(s) => s,
Err(e) => {
return err(
MigrationArea::Config,
format!("Failed to serialize config: {e}"),
);
}
};
if let Err(e) = fs::write(&oc_config_path, &json) {
return err(
MigrationArea::Config,
format!("Failed to write legacy.json: {e}"),
);
}
AreaResult {
area: MigrationArea::Config,
success: true,
items_processed: 1,
warnings,
error: None,
}
}