{"repo": "another-s347/remi-cat", "files": {"src/command/setup.rs": {"6f46abb0faef91d154db3bc3f83ae9db173802e2": {"t": 1785745872.0046067, "n": 16722, "h": "8a31c3a2b353b70b", "c": "use std::path::{Path, PathBuf};\nuse std::rc::Rc;\nuse std::sync::Arc;\n\nuse anyhow::Context;\nuse futures::StreamExt;\nuse tokio::sync::Mutex;\n\nuse crate::config::{\n detect_setup_state, upsert_dotenv_value, write_runtime_config, FeishuTransport, ImMode,\n RuntimeConfig, RuntimeSandboxKind, SetupState,\n};\nuse crate::instance_profile::InstanceProfile;\nuse crate::profile_command::{\n available_container_name, configured_ports, first_available_port, print_port_adjustment,\n};\nuse crate::secret_store::SecretStore;\nuse bot_core::{\n install_embedded_agent_profiles, install_embedded_model_profiles,\n validate_model_profile_api_key, AgentProfile, AgentRegistry, CatBotBuilder, CatEvent, Content,\n ModelProfileConfig, ModelProfileRegistry, StreamOptions,\n};\n\npub(crate) async fn run_setup(\n profile: &InstanceProfile,\n data_dir: &mut PathBuf,\n secret_store: Arc>,\n) -> anyhow::Result<()> {\n println!(\"remi-cat setup\");\n println!(\"Profile: {}\", profile.label());\n println!(\"This wizard will configure the local runtime and verify one real chat round.\\n\");\n\n if !profile.is_named() {\n let chosen_dir = prompt_with_default(\"Data dir\", &data_dir.display().to_string())?;\n *data_dir = PathBuf::from(chosen_dir);\n } else {\n println!(\"Data dir: {}\\n\", data_dir.display());\n }\n std::fs::create_dir_all(&data_dir)?;\n let existing_config = match detect_setup_state(data_dir) {\n SetupState::Initialized {\n config_path,\n config,\n } => {\n println!(\n \"Existing setup found at {}. Press Enter to keep current values or type new ones.\\n\",\n config_path.display()\n );\n Some(config)\n }\n SetupState::Invalid { config_path, error } => {\n println!(\n \"Existing setup config at {} is invalid and will be replaced: {error}\\n\",\n config_path.display()\n );\n None\n }\n _ => None,\n };\n install_embedded_model_profiles(data_dir.join(\"models\"))?;\n install_embedded_agent_profiles(data_dir.join(\"agents\"))?;\n std::fs::create_dir_all(data_dir.join(\"workflows\"))?;\n\n let agents_dir = data_dir.join(\"agents\");\n let models_dir = data_dir.join(\"models\");\n let agent_registry = AgentRegistry::load(&agents_dir)?;\n let model_registry = ModelProfileRegistry::load(&models_dir)?;\n let mut agents: Vec = agent_registry.profiles().cloned().collect();\n agents.sort_by(|a, b| a.id.cmp(&b.id));\n let mut models = model_registry.list();\n models.sort_by(|a, b| a.id.cmp(&b.id));\n\n let root_agent_id = choose_from_list(\n \"Root agent\",\n &agents\n .iter()\n .map(|profile| format!(\"{} - {}\", profile.id, profile.description))\n .collect::>(),\n existing_config\n .as_ref()\n .map(|config| config.root_agent_id.as_str())\n .unwrap_or(\"default\"),\n )?;\n let root_agent = agents\n .iter()\n .find(|profile| profile.id == root_agent_id)\n .ok_or_else(|| anyhow::anyhow!(\"selected root agent `{root_agent_id}` no longer exists\"))?;\n\n let default_model_id = root_agent\n .models\n .primary\n .clone()\n .unwrap_or_else(|| \"default\".to_string());\n let model_options = models\n .iter()\n .map(|profile| {\n format!(\n \"{} - {} [{}]\",\n profile.id,\n profile.name,\n model_key_status_label(profile)\n )\n })\n .collect::>();\n let model_profile = choose_from_list(\n \"Primary model profile\",\n &model_options,\n existing_config\n .as_ref()\n .map(|config| config.model_profile.as_str())\n .unwrap_or(&default_model_id),\n )?;\n let selected_model_profile = models\n .iter()\n .find(|profile| profile.id == model_profile)\n .ok_or_else(|| {\n anyhow::anyhow!(\"selected model profile `{model_profile}` no longer exists\")\n })?;\n let api_key_env_key = api_key_env_key_for_profile(selected_model_profile);\n let default_sandbox_kind = existing_config\n .as_ref()\n .map(|config| config.sandbox.kind.as_env_value())\n .unwrap_or(\"no_sandbox\");\n let sandbox_kind = choose_from_list(\n \"Sandbox\",\n &[\n \"disabled - fs only; bash tool is not exposed\".to_string(),\n \"no_sandbox - local fs and local bash in the configured data dir\".to_string(),\n \"docker - fs in a host dir mounted into a persistent Docker container\".to_string(),\n ],\n default_sandbox_kind,\n )?;\n let default_feishu_transport = existing_config\n .as_ref()\n .map(|config| config.im.transport.as_env_value())\n .unwrap_or(\"websocket\");\n let feishu_transport = choose_from_list(\n \"Feishu inbound transport\",\n &[\n \"websocket - Feishu long connection; no public callback URL needed\".to_string(),\n \"event_hook - HTTP callback endpoint; requires a public URL/proxy in Feishu app settings\".to_string(),\n ],\n default_feishu_transport,\n )?;\n\n let current_api_key = api_key_env_keys_for_profile(selected_model_profile)\n .into_iter()\n .find_map(|key| {\n std::env::var(key)\n .ok()\n .filter(|value| !value.trim().is_empty())\n });\n let api_key = prompt_secret(current_api_key.as_deref())?;\n if !api_key.trim().is_empty() {\n unsafe {\n std::env::set_var(&api_key_env_key, api_key.trim());\n }\n }\n validate_model_profile_api_key(selected_model_profile).await?;\n\n let previous_config = existing_config.clone();\n let mut config = existing_config.unwrap_or_else(|| RuntimeConfig::default_for(data_dir));\n config.data_dir = data_dir.display().to_string();\n config.root_agent_id = root_agent_id.clone();\n config.model_profile = model_profile.clone();\n if profile.is_named() && previous_config.is_none() {\n config.sandbox.container_name = format!(\"remi-cat-sandbox-{}\", profile.label());\n }\n match sandbox_kind.as_str() {\n \"docker\" => {\n config.sandbox.kind = RuntimeSandboxKind::Docker;\n let default_host_dir = config.sandbox.host_dir_or_data_dir(&config.data_dir);\n config.sandbox.host_dir = prompt_with_default(\"Sandbox host dir\", &default_host_dir)?;\n config.sandbox.container_dir =\n prompt_with_default(\"Sandbox container dir\", &config.sandbox.container_dir)?;\n config.sandbox.image = prompt_with_default(\"Sandbox image\", &config.sandbox.image)?;\n config.sandbox.container_name =\n prompt_with_default(\"Sandbox container name\", &config.sandbox.container_name)?;\n config.sandbox.container_name =\n available_container_name(&config.sandbox.container_name, data_dir)?;\n }\n _ => {\n config.sandbox.kind = if sandbox_kind == \"disabled\" {\n RuntimeSandboxKind::Disabled\n } else {\n RuntimeSandboxKind::NoSandbox\n };\n config.sandbox.host_dir = data_dir.display().to_string();\n }\n }\n config.admin.enabled = prompt_bool_with_default(\"Admin enabled\", config.admin.enabled)?;\n if config.admin.enabled {\n config.admin.host = prompt_with_default(\"Admin listen host\", &config.admin.host)?;\n config.admin.port =\n prompt_with_default(\"Admin listen port\", &config.admin.port.to_string())?\n .parse()\n .context(\"invalid Admin listen port\")?;\n }\n config.im.transport = match feishu_transport.as_str() {\n \"event_hook\" | \"event-hook\" | \"hook\" => FeishuTransport::EventHook,\n _ => FeishuTransport::WebSocket,\n };\n config.im.mode = ImMode::Feishu;\n if matches!(config.im.transport, FeishuTransport::EventHook) {\n config.im.event_hook.host =\n prompt_with_default(\"Feishu Event Hook listen host\", &config.im.event_hook.host)?;\n config.im.event_hook.port = prompt_with_default(\n \"Feishu Event Hook listen port\",\n &config.im.event_hook.port.to_string(),\n )?\n .parse()\n .context(\"invalid Feishu Event Hook listen port\")?;\n config.im.event_hook.path =\n prompt_with_default(\"Feishu Event Hook path\", &config.im.event_hook.path)?;\n config.im.event_hook.verification_token = prompt_with_default(\n \"Feishu Event Hook verification token (blank to disable local token check)\",\n &config.im.event_hook.verification_token,\n )?;\n }\n\n let mut reserved_ports = configured_ports(data_dir)?;\n if config.admin.enabled {\n let requested = config.admin.port;\n config.admin.port = first_available_port(&config.admin.host, requested, &reserved_ports)?;\n print_port_adjustment(\"Admin\", requested, config.admin.port);\n reserved_ports.insert(config.admin.port);\n }\n if matches!(config.im.transport, FeishuTransport::EventHook) {\n let requested = config.im.event_hook.port;\n config.im.event_hook.port =\n first_available_port(&config.im.event_hook.host, requested, &reserved_ports)?;\n print_port_adjustment(\"Feishu Event Hook\", requested, config.im.event_hook.port);\n }\n\n let runtime_path = write_runtime_config(data_dir, &config)?;\n match run_setup_smoke(data_dir, &config, &api_key_env_key, api_key.trim()).await {\n Ok(reply) => {\n let env_path = PathBuf::from(\".env\");\n if !profile.is_named() {\n upsert_dotenv_value(&env_path, \"REMI_DATA_DIR\", &config.data_dir)?;\n }\n if !api_key.trim().is_empty() {\n secret_store\n .lock()\n .await\n .set(&api_key_env_key, api_key.trim())?;\n }\n println!(\"\\nSetup verification succeeded.\");\n println!(\"Smoke reply: {}\", reply.trim());\n println!(\"Saved runtime config to {}\", runtime_path.display());\n println!(\"\\nNext steps:\");\n println!(\n \"- Local chat: remi-cat{} cli --channel support --user alice --name Alice \\\"Hello\\\"\",\n profile\n .name\n .as_ref()\n .map(|name| format!(\" --profile {name}\"))\n .unwrap_or_default()\n );\n println!(\"- Feishu: run `remi-cat feishu init`, then run remi-cat\");\n println!(\"- ACP: configure ACP settings later when needed\");\n println!(\"- Shell: enable shell.mode only when you want local bash tools\");\n Ok(())\n }\n Err(err) => {\n if let Some(previous_config) = previous_config {\n let _ = write_runtime_config(data_dir, &previous_config);\n } else {\n let _ = std::fs::remove_file(&runtime_path);\n }\n anyhow::bail!(\"setup verification failed: {err:#}\")\n }\n }\n}\n\nasync fn run_setup_smoke(\n data_dir: &Path,\n config: &RuntimeConfig,\n api_key_env_key: &str,\n api_key: &str,\n) -> anyhow::Result {\n unsafe {\n std::env::set_var(\"REMI_DATA_DIR\", &config.data_dir);\n std::env::set_var(\"REMI_AGENT_ID\", &config.root_agent_id);\n std::env::set_var(\"REMI_MODEL_PROFILE\", &config.model_profile);\n std::env::set_var(\"REMI_SANDBOX_KIND\", config.sandbox.kind.as_env_value());\n std::env::set_var(\n \"REMI_SANDBOX_HOST_DIR\",\n config.sandbox.host_dir_or_data_dir(&config.data_dir),\n );\n std::env::set_var(\"REMI_SANDBOX_CONTAINER_DIR\", &config.sandbox.container_dir);\n std::env::set_var(\"REMI_SANDBOX_IMAGE\", &config.sandbox.image);\n std::env::set_var(\n \"REMI_SANDBOX_CONTAINER_NAME\",\n &config.sandbox.container_name,\n );\n if !api_key.trim().is_empty() {\n std::env::set_var(api_key_env_key, api_key.trim());\n }\n }\n let bot = Rc::new(CatBotBuilder::from_env()?.build()?);\n let session_id = \"setup-smoke\";\n let mut output = String::new();\n let opts = StreamOptions::default();\n let mut stream = std::pin::pin!(bot.stream_with_options(\n session_id,\n Content::text(format!(\n \"You are being verified for setup. Reply with one short sentence that says setup smoke passed for agent `{}`.\",\n config.root_agent_id\n )),\n opts\n ));\n while let Some(event) = stream.next().await {\n match event {\n CatEvent::Text(delta) => output.push_str(&delta),\n CatEvent::Error(err) => anyhow::bail!(err.to_string()),\n CatEvent::Done => break,\n _ => {}\n }\n }\n if output.trim().is_empty() {\n anyhow::bail!(\"model returned an empty reply\")\n }\n let _ = std::fs::remove_dir_all(data_dir.join(\"memory\").join(session_id));\n Ok(output)\n}\n\nfn prompt_with_default(label: &str, default: &str) -> anyhow::Result {\n crate::tui_form::text_input(label, default, false)\n}\n\nfn choose_from_list(label: &str, options: &[String], default_id: &str) -> anyhow::Result {\n let default_index = options\n .iter()\n .position(|option| option_id(option) == default_id)\n .unwrap_or(0);\n crate::tui_form::select(label, options, default_index).map(|selected| option_id(&selected))\n}\n\nfn prompt_bool_with_default(label: &str, default: bool) -> anyhow::Result {\n crate::tui_form::bool_input(label, default)\n}\n\nfn option_id(option: &str) -> String {\n option\n .split_once(\" - \")\n .map(|(id, _)| id)\n .unwrap_or(option)\n .trim()\n .to_string()\n}\n\nfn api_key_env_key_for_profile(profile: &ModelProfileConfig) -> String {\n api_key_env_keys_for_profile(profile)\n .into_iter()\n .next()\n .unwrap_or_else(|| \"OPENAI_API_KEY\".to_string())\n}\n\nfn model_key_status_label(profile: &ModelProfileConfig) -> String {\n let keys = api_key_env_keys_for_profile(profile);\n if keys.iter().any(|key| {\n std::env::var(key)\n .map(|value| !value.trim().is_empty())\n .unwrap_or(false)\n }) {\n \"key ok\".to_string()\n } else {\n format!(\"key missing: {}\", keys.join(\" or \"))\n }\n}\n\nfn api_key_env_keys_for_profile(profile: &ModelProfileConfig) -> Vec {\n let provider = profile\n .provider\n .as_deref()\n .unwrap_or_default()\n .trim()\n .to_ascii_lowercase();\n let model = profile.model.to_ascii_lowercase();\n let base_url = profile\n .base_url\n .as_deref()\n .unwrap_or_default()\n .to_ascii_lowercase();\n\n if provider == \"deepseek\" || model.contains(\"deepseek\") || base_url.contains(\"deepseek.com\") {\n vec![\"DEEPSEEK_API_KEY\".to_string()]\n } else if provider == \"mimo\" || model.contains(\"mimo\") || base_url.contains(\"xiaomimimo.com\") {\n vec![\"MIMO_API_KEY\".to_string()]\n } else if provider == \"kimi\"\n || provider == \"moonshot\"\n || model.contains(\"kimi\")\n || base_url.contains(\"moonshot.cn\")\n {\n vec![\"MOONSHOT_API_KEY\".to_string(), \"KIMI_API_KEY\".to_string()]\n } else if provider == \"glm\"\n || provider == \"zhipu\"\n || provider == \"bigmodel\"\n || model.contains(\"glm\")\n || base_url.contains(\"bigmodel.cn\")\n {\n vec![\n \"GLM_API_KEY\".to_string(),\n \"ZHIPU_API_KEY\".to_string(),\n \"BIGMODEL_API_KEY\".to_string(),\n ]\n } else if provider == \"openai\"\n || provider.is_empty()\n || model.starts_with(\"gpt-\")\n || model.starts_with('o')\n || base_url.contains(\"api.openai.com\")\n {\n vec![\"OPENAI_API_KEY\".to_string()]\n } else if !provider.is_empty() {\n vec![format!(\"{}_API_KEY\", env_key_fragment(&provider))]\n } else {\n vec![\"OPENAI_API_KEY\".to_string()]\n }\n}\n\nfn env_key_fragment(raw: &str) -> String {\n let mut out = String::new();\n for ch in raw.chars() {\n if ch.is_ascii_alphanumeric() {\n out.push(ch.to_ascii_uppercase());\n } else if !out.ends_with('_') {\n out.push('_');\n }\n }\n out.trim_matches('_').to_string()\n}\n\nfn prompt_secret(current: Option<&str>) -> anyhow::Result {\n let label = if current.is_some() {\n \"OpenAI-compatible API key (blank keeps current)\"\n } else {\n \"OpenAI-compatible API key\"\n };\n crate::tui_form::text_input(label, current.unwrap_or_default(), true)\n}\n"}, "35d7c374576fadf2601906232b9300d76c20f40b": {"t": 1785757450.1530488, "n": 16722, "h": "8a31c3a2b353b70b", "c": "use std::path::{Path, PathBuf};\nuse std::rc::Rc;\nuse std::sync::Arc;\n\nuse anyhow::Context;\nuse futures::StreamExt;\nuse tokio::sync::Mutex;\n\nuse crate::config::{\n detect_setup_state, upsert_dotenv_value, write_runtime_config, FeishuTransport, ImMode,\n RuntimeConfig, RuntimeSandboxKind, SetupState,\n};\nuse crate::instance_profile::InstanceProfile;\nuse crate::profile_command::{\n available_container_name, configured_ports, first_available_port, print_port_adjustment,\n};\nuse crate::secret_store::SecretStore;\nuse bot_core::{\n install_embedded_agent_profiles, install_embedded_model_profiles,\n validate_model_profile_api_key, AgentProfile, AgentRegistry, CatBotBuilder, CatEvent, Content,\n ModelProfileConfig, ModelProfileRegistry, StreamOptions,\n};\n\npub(crate) async fn run_setup(\n profile: &InstanceProfile,\n data_dir: &mut PathBuf,\n secret_store: Arc>,\n) -> anyhow::Result<()> {\n println!(\"remi-cat setup\");\n println!(\"Profile: {}\", profile.label());\n println!(\"This wizard will configure the local runtime and verify one real chat round.\\n\");\n\n if !profile.is_named() {\n let chosen_dir = prompt_with_default(\"Data dir\", &data_dir.display().to_string())?;\n *data_dir = PathBuf::from(chosen_dir);\n } else {\n println!(\"Data dir: {}\\n\", data_dir.display());\n }\n std::fs::create_dir_all(&data_dir)?;\n let existing_config = match detect_setup_state(data_dir) {\n SetupState::Initialized {\n config_path,\n config,\n } => {\n println!(\n \"Existing setup found at {}. Press Enter to keep current values or type new ones.\\n\",\n config_path.display()\n );\n Some(config)\n }\n SetupState::Invalid { config_path, error } => {\n println!(\n \"Existing setup config at {} is invalid and will be replaced: {error}\\n\",\n config_path.display()\n );\n None\n }\n _ => None,\n };\n install_embedded_model_profiles(data_dir.join(\"models\"))?;\n install_embedded_agent_profiles(data_dir.join(\"agents\"))?;\n std::fs::create_dir_all(data_dir.join(\"workflows\"))?;\n\n let agents_dir = data_dir.join(\"agents\");\n let models_dir = data_dir.join(\"models\");\n let agent_registry = AgentRegistry::load(&agents_dir)?;\n let model_registry = ModelProfileRegistry::load(&models_dir)?;\n let mut agents: Vec = agent_registry.profiles().cloned().collect();\n agents.sort_by(|a, b| a.id.cmp(&b.id));\n let mut models = model_registry.list();\n models.sort_by(|a, b| a.id.cmp(&b.id));\n\n let root_agent_id = choose_from_list(\n \"Root agent\",\n &agents\n .iter()\n .map(|profile| format!(\"{} - {}\", profile.id, profile.description))\n .collect::>(),\n existing_config\n .as_ref()\n .map(|config| config.root_agent_id.as_str())\n .unwrap_or(\"default\"),\n )?;\n let root_agent = agents\n .iter()\n .find(|profile| profile.id == root_agent_id)\n .ok_or_else(|| anyhow::anyhow!(\"selected root agent `{root_agent_id}` no longer exists\"))?;\n\n let default_model_id = root_agent\n .models\n .primary\n .clone()\n .unwrap_or_else(|| \"default\".to_string());\n let model_options = models\n .iter()\n .map(|profile| {\n format!(\n \"{} - {} [{}]\",\n profile.id,\n profile.name,\n model_key_status_label(profile)\n )\n })\n .collect::>();\n let model_profile = choose_from_list(\n \"Primary model profile\",\n &model_options,\n existing_config\n .as_ref()\n .map(|config| config.model_profile.as_str())\n .unwrap_or(&default_model_id),\n )?;\n let selected_model_profile = models\n .iter()\n .find(|profile| profile.id == model_profile)\n .ok_or_else(|| {\n anyhow::anyhow!(\"selected model profile `{model_profile}` no longer exists\")\n })?;\n let api_key_env_key = api_key_env_key_for_profile(selected_model_profile);\n let default_sandbox_kind = existing_config\n .as_ref()\n .map(|config| config.sandbox.kind.as_env_value())\n .unwrap_or(\"no_sandbox\");\n let sandbox_kind = choose_from_list(\n \"Sandbox\",\n &[\n \"disabled - fs only; bash tool is not exposed\".to_string(),\n \"no_sandbox - local fs and local bash in the configured data dir\".to_string(),\n \"docker - fs in a host dir mounted into a persistent Docker container\".to_string(),\n ],\n default_sandbox_kind,\n )?;\n let default_feishu_transport = existing_config\n .as_ref()\n .map(|config| config.im.transport.as_env_value())\n .unwrap_or(\"websocket\");\n let feishu_transport = choose_from_list(\n \"Feishu inbound transport\",\n &[\n \"websocket - Feishu long connection; no public callback URL needed\".to_string(),\n \"event_hook - HTTP callback endpoint; requires a public URL/proxy in Feishu app settings\".to_string(),\n ],\n default_feishu_transport,\n )?;\n\n let current_api_key = api_key_env_keys_for_profile(selected_model_profile)\n .into_iter()\n .find_map(|key| {\n std::env::var(key)\n .ok()\n .filter(|value| !value.trim().is_empty())\n });\n let api_key = prompt_secret(current_api_key.as_deref())?;\n if !api_key.trim().is_empty() {\n unsafe {\n std::env::set_var(&api_key_env_key, api_key.trim());\n }\n }\n validate_model_profile_api_key(selected_model_profile).await?;\n\n let previous_config = existing_config.clone();\n let mut config = existing_config.unwrap_or_else(|| RuntimeConfig::default_for(data_dir));\n config.data_dir = data_dir.display().to_string();\n config.root_agent_id = root_agent_id.clone();\n config.model_profile = model_profile.clone();\n if profile.is_named() && previous_config.is_none() {\n config.sandbox.container_name = format!(\"remi-cat-sandbox-{}\", profile.label());\n }\n match sandbox_kind.as_str() {\n \"docker\" => {\n config.sandbox.kind = RuntimeSandboxKind::Docker;\n let default_host_dir = config.sandbox.host_dir_or_data_dir(&config.data_dir);\n config.sandbox.host_dir = prompt_with_default(\"Sandbox host dir\", &default_host_dir)?;\n config.sandbox.container_dir =\n prompt_with_default(\"Sandbox container dir\", &config.sandbox.container_dir)?;\n config.sandbox.image = prompt_with_default(\"Sandbox image\", &config.sandbox.image)?;\n config.sandbox.container_name =\n prompt_with_default(\"Sandbox container name\", &config.sandbox.container_name)?;\n config.sandbox.container_name =\n available_container_name(&config.sandbox.container_name, data_dir)?;\n }\n _ => {\n config.sandbox.kind = if sandbox_kind == \"disabled\" {\n RuntimeSandboxKind::Disabled\n } else {\n RuntimeSandboxKind::NoSandbox\n };\n config.sandbox.host_dir = data_dir.display().to_string();\n }\n }\n config.admin.enabled = prompt_bool_with_default(\"Admin enabled\", config.admin.enabled)?;\n if config.admin.enabled {\n config.admin.host = prompt_with_default(\"Admin listen host\", &config.admin.host)?;\n config.admin.port =\n prompt_with_default(\"Admin listen port\", &config.admin.port.to_string())?\n .parse()\n .context(\"invalid Admin listen port\")?;\n }\n config.im.transport = match feishu_transport.as_str() {\n \"event_hook\" | \"event-hook\" | \"hook\" => FeishuTransport::EventHook,\n _ => FeishuTransport::WebSocket,\n };\n config.im.mode = ImMode::Feishu;\n if matches!(config.im.transport, FeishuTransport::EventHook) {\n config.im.event_hook.host =\n prompt_with_default(\"Feishu Event Hook listen host\", &config.im.event_hook.host)?;\n config.im.event_hook.port = prompt_with_default(\n \"Feishu Event Hook listen port\",\n &config.im.event_hook.port.to_string(),\n )?\n .parse()\n .context(\"invalid Feishu Event Hook listen port\")?;\n config.im.event_hook.path =\n prompt_with_default(\"Feishu Event Hook path\", &config.im.event_hook.path)?;\n config.im.event_hook.verification_token = prompt_with_default(\n \"Feishu Event Hook verification token (blank to disable local token check)\",\n &config.im.event_hook.verification_token,\n )?;\n }\n\n let mut reserved_ports = configured_ports(data_dir)?;\n if config.admin.enabled {\n let requested = config.admin.port;\n config.admin.port = first_available_port(&config.admin.host, requested, &reserved_ports)?;\n print_port_adjustment(\"Admin\", requested, config.admin.port);\n reserved_ports.insert(config.admin.port);\n }\n if matches!(config.im.transport, FeishuTransport::EventHook) {\n let requested = config.im.event_hook.port;\n config.im.event_hook.port =\n first_available_port(&config.im.event_hook.host, requested, &reserved_ports)?;\n print_port_adjustment(\"Feishu Event Hook\", requested, config.im.event_hook.port);\n }\n\n let runtime_path = write_runtime_config(data_dir, &config)?;\n match run_setup_smoke(data_dir, &config, &api_key_env_key, api_key.trim()).await {\n Ok(reply) => {\n let env_path = PathBuf::from(\".env\");\n if !profile.is_named() {\n upsert_dotenv_value(&env_path, \"REMI_DATA_DIR\", &config.data_dir)?;\n }\n if !api_key.trim().is_empty() {\n secret_store\n .lock()\n .await\n .set(&api_key_env_key, api_key.trim())?;\n }\n println!(\"\\nSetup verification succeeded.\");\n println!(\"Smoke reply: {}\", reply.trim());\n println!(\"Saved runtime config to {}\", runtime_path.display());\n println!(\"\\nNext steps:\");\n println!(\n \"- Local chat: remi-cat{} cli --channel support --user alice --name Alice \\\"Hello\\\"\",\n profile\n .name\n .as_ref()\n .map(|name| format!(\" --profile {name}\"))\n .unwrap_or_default()\n );\n println!(\"- Feishu: run `remi-cat feishu init`, then run remi-cat\");\n println!(\"- ACP: configure ACP settings later when needed\");\n println!(\"- Shell: enable shell.mode only when you want local bash tools\");\n Ok(())\n }\n Err(err) => {\n if let Some(previous_config) = previous_config {\n let _ = write_runtime_config(data_dir, &previous_config);\n } else {\n let _ = std::fs::remove_file(&runtime_path);\n }\n anyhow::bail!(\"setup verification failed: {err:#}\")\n }\n }\n}\n\nasync fn run_setup_smoke(\n data_dir: &Path,\n config: &RuntimeConfig,\n api_key_env_key: &str,\n api_key: &str,\n) -> anyhow::Result {\n unsafe {\n std::env::set_var(\"REMI_DATA_DIR\", &config.data_dir);\n std::env::set_var(\"REMI_AGENT_ID\", &config.root_agent_id);\n std::env::set_var(\"REMI_MODEL_PROFILE\", &config.model_profile);\n std::env::set_var(\"REMI_SANDBOX_KIND\", config.sandbox.kind.as_env_value());\n std::env::set_var(\n \"REMI_SANDBOX_HOST_DIR\",\n config.sandbox.host_dir_or_data_dir(&config.data_dir),\n );\n std::env::set_var(\"REMI_SANDBOX_CONTAINER_DIR\", &config.sandbox.container_dir);\n std::env::set_var(\"REMI_SANDBOX_IMAGE\", &config.sandbox.image);\n std::env::set_var(\n \"REMI_SANDBOX_CONTAINER_NAME\",\n &config.sandbox.container_name,\n );\n if !api_key.trim().is_empty() {\n std::env::set_var(api_key_env_key, api_key.trim());\n }\n }\n let bot = Rc::new(CatBotBuilder::from_env()?.build()?);\n let session_id = \"setup-smoke\";\n let mut output = String::new();\n let opts = StreamOptions::default();\n let mut stream = std::pin::pin!(bot.stream_with_options(\n session_id,\n Content::text(format!(\n \"You are being verified for setup. Reply with one short sentence that says setup smoke passed for agent `{}`.\",\n config.root_agent_id\n )),\n opts\n ));\n while let Some(event) = stream.next().await {\n match event {\n CatEvent::Text(delta) => output.push_str(&delta),\n CatEvent::Error(err) => anyhow::bail!(err.to_string()),\n CatEvent::Done => break,\n _ => {}\n }\n }\n if output.trim().is_empty() {\n anyhow::bail!(\"model returned an empty reply\")\n }\n let _ = std::fs::remove_dir_all(data_dir.join(\"memory\").join(session_id));\n Ok(output)\n}\n\nfn prompt_with_default(label: &str, default: &str) -> anyhow::Result {\n crate::tui_form::text_input(label, default, false)\n}\n\nfn choose_from_list(label: &str, options: &[String], default_id: &str) -> anyhow::Result {\n let default_index = options\n .iter()\n .position(|option| option_id(option) == default_id)\n .unwrap_or(0);\n crate::tui_form::select(label, options, default_index).map(|selected| option_id(&selected))\n}\n\nfn prompt_bool_with_default(label: &str, default: bool) -> anyhow::Result {\n crate::tui_form::bool_input(label, default)\n}\n\nfn option_id(option: &str) -> String {\n option\n .split_once(\" - \")\n .map(|(id, _)| id)\n .unwrap_or(option)\n .trim()\n .to_string()\n}\n\nfn api_key_env_key_for_profile(profile: &ModelProfileConfig) -> String {\n api_key_env_keys_for_profile(profile)\n .into_iter()\n .next()\n .unwrap_or_else(|| \"OPENAI_API_KEY\".to_string())\n}\n\nfn model_key_status_label(profile: &ModelProfileConfig) -> String {\n let keys = api_key_env_keys_for_profile(profile);\n if keys.iter().any(|key| {\n std::env::var(key)\n .map(|value| !value.trim().is_empty())\n .unwrap_or(false)\n }) {\n \"key ok\".to_string()\n } else {\n format!(\"key missing: {}\", keys.join(\" or \"))\n }\n}\n\nfn api_key_env_keys_for_profile(profile: &ModelProfileConfig) -> Vec {\n let provider = profile\n .provider\n .as_deref()\n .unwrap_or_default()\n .trim()\n .to_ascii_lowercase();\n let model = profile.model.to_ascii_lowercase();\n let base_url = profile\n .base_url\n .as_deref()\n .unwrap_or_default()\n .to_ascii_lowercase();\n\n if provider == \"deepseek\" || model.contains(\"deepseek\") || base_url.contains(\"deepseek.com\") {\n vec![\"DEEPSEEK_API_KEY\".to_string()]\n } else if provider == \"mimo\" || model.contains(\"mimo\") || base_url.contains(\"xiaomimimo.com\") {\n vec![\"MIMO_API_KEY\".to_string()]\n } else if provider == \"kimi\"\n || provider == \"moonshot\"\n || model.contains(\"kimi\")\n || base_url.contains(\"moonshot.cn\")\n {\n vec![\"MOONSHOT_API_KEY\".to_string(), \"KIMI_API_KEY\".to_string()]\n } else if provider == \"glm\"\n || provider == \"zhipu\"\n || provider == \"bigmodel\"\n || model.contains(\"glm\")\n || base_url.contains(\"bigmodel.cn\")\n {\n vec![\n \"GLM_API_KEY\".to_string(),\n \"ZHIPU_API_KEY\".to_string(),\n \"BIGMODEL_API_KEY\".to_string(),\n ]\n } else if provider == \"openai\"\n || provider.is_empty()\n || model.starts_with(\"gpt-\")\n || model.starts_with('o')\n || base_url.contains(\"api.openai.com\")\n {\n vec![\"OPENAI_API_KEY\".to_string()]\n } else if !provider.is_empty() {\n vec![format!(\"{}_API_KEY\", env_key_fragment(&provider))]\n } else {\n vec![\"OPENAI_API_KEY\".to_string()]\n }\n}\n\nfn env_key_fragment(raw: &str) -> String {\n let mut out = String::new();\n for ch in raw.chars() {\n if ch.is_ascii_alphanumeric() {\n out.push(ch.to_ascii_uppercase());\n } else if !out.ends_with('_') {\n out.push('_');\n }\n }\n out.trim_matches('_').to_string()\n}\n\nfn prompt_secret(current: Option<&str>) -> anyhow::Result {\n let label = if current.is_some() {\n \"OpenAI-compatible API key (blank keeps current)\"\n } else {\n \"OpenAI-compatible API key\"\n };\n crate::tui_form::text_input(label, current.unwrap_or_default(), true)\n}\n"}, "1818f0accdc1705cce2283d70db08a32933ab978": {"t": 1785757451.3780081, "n": 15839, "h": "9f4280f83299fb4e", "c": "use std::io::{self, Write};\nuse std::path::{Path, PathBuf};\nuse std::rc::Rc;\nuse std::sync::Arc;\n\nuse anyhow::Context;\nuse futures::StreamExt;\nuse tokio::sync::Mutex;\n\nuse crate::config::{\n detect_setup_state, upsert_dotenv_value, write_runtime_config, FeishuTransport, ImMode,\n RuntimeConfig, RuntimeSandboxKind, SetupState,\n};\nuse crate::instance_profile::InstanceProfile;\nuse crate::profile_command::{\n available_container_name, configured_ports, first_available_port, print_port_adjustment,\n};\nuse crate::secret_store::SecretStore;\nuse bot_core::{\n install_embedded_agent_profiles, install_embedded_model_profiles, AgentProfile, AgentRegistry,\n CatBotBuilder, CatEvent, Content, ModelProfileConfig, ModelProfileRegistry, StreamOptions,\n};\n\npub(crate) async fn run_setup(\n profile: &InstanceProfile,\n data_dir: &mut PathBuf,\n secret_store: Arc>,\n) -> anyhow::Result<()> {\n println!(\"remi-cat setup\");\n println!(\"Profile: {}\", profile.label());\n println!(\"This wizard will configure the local runtime and verify one real chat round.\\n\");\n\n if !profile.is_named() {\n let chosen_dir = prompt_with_default(\"Data dir\", &data_dir.display().to_string())?;\n *data_dir = PathBuf::from(chosen_dir);\n } else {\n println!(\"Data dir: {}\\n\", data_dir.display());\n }\n std::fs::create_dir_all(&data_dir)?;\n let existing_config = match detect_setup_state(data_dir) {\n SetupState::Initialized {\n config_path,\n config,\n } => {\n println!(\n \"Existing setup found at {}. Press Enter to keep current values or type new ones.\\n\",\n config_path.display()\n );\n Some(config)\n }\n SetupState::Invalid { config_path, error } => {\n println!(\n \"Existing setup config at {} is invalid and will be replaced: {error}\\n\",\n config_path.display()\n );\n None\n }\n _ => None,\n };\n install_embedded_model_profiles(data_dir.join(\"models\"))?;\n install_embedded_agent_profiles(data_dir.join(\"agents\"))?;\n std::fs::create_dir_all(data_dir.join(\"workflows\"))?;\n\n let agents_dir = data_dir.join(\"agents\");\n let models_dir = data_dir.join(\"models\");\n let agent_registry = AgentRegistry::load(&agents_dir)?;\n let model_registry = ModelProfileRegistry::load(&models_dir)?;\n let mut agents: Vec = agent_registry.profiles().cloned().collect();\n agents.sort_by(|a, b| a.id.cmp(&b.id));\n let mut models = model_registry.list();\n models.sort_by(|a, b| a.id.cmp(&b.id));\n\n let root_agent_id = choose_from_list(\n \"Root agent\",\n &agents\n .iter()\n .map(|profile| format!(\"{} - {}\", profile.id, profile.description))\n .collect::>(),\n existing_config\n .as_ref()\n .map(|config| config.root_agent_id.as_str())\n .unwrap_or(\"default\"),\n )?;\n let root_agent = agents\n .iter()\n .find(|profile| profile.id == root_agent_id)\n .ok_or_else(|| anyhow::anyhow!(\"selected root agent `{root_agent_id}` no longer exists\"))?;\n\n let default_model_id = root_agent\n .models\n .primary\n .clone()\n .unwrap_or_else(|| \"default\".to_string());\n let model_profile = choose_from_list(\n \"Primary model profile\",\n &models\n .iter()\n .map(|profile| format!(\"{} - {}\", profile.id, profile.name))\n .collect::>(),\n existing_config\n .as_ref()\n .map(|config| config.model_profile.as_str())\n .unwrap_or(&default_model_id),\n )?;\n let selected_model_profile = models\n .iter()\n .find(|profile| profile.id == model_profile)\n .ok_or_else(|| {\n anyhow::anyhow!(\"selected model profile `{model_profile}` no longer exists\")\n })?;\n let api_key_env_key = api_key_env_key_for_profile(selected_model_profile);\n let default_sandbox_kind = existing_config\n .as_ref()\n .map(|config| config.sandbox.kind.as_env_value())\n .unwrap_or(\"no_sandbox\");\n let sandbox_kind = choose_from_list(\n \"Sandbox\",\n &[\n \"disabled - fs only; bash tool is not exposed\".to_string(),\n \"no_sandbox - local fs and local bash in the configured data dir\".to_string(),\n \"docker - fs in a host dir mounted into a persistent Docker container\".to_string(),\n ],\n default_sandbox_kind,\n )?;\n let default_feishu_transport = existing_config\n .as_ref()\n .map(|config| config.im.transport.as_env_value())\n .unwrap_or(\"websocket\");\n let feishu_transport = choose_from_list(\n \"Feishu inbound transport\",\n &[\n \"websocket - Feishu long connection; no public callback URL needed\".to_string(),\n \"event_hook - HTTP callback endpoint; requires a public URL/proxy in Feishu app settings\".to_string(),\n ],\n default_feishu_transport,\n )?;\n\n let current_api_key = api_key_env_keys_for_profile(selected_model_profile)\n .into_iter()\n .find_map(|key| {\n std::env::var(key)\n .ok()\n .filter(|value| !value.trim().is_empty())\n });\n let api_key = prompt_secret(current_api_key.as_deref())?;\n if !api_key.trim().is_empty() {\n unsafe {\n std::env::set_var(api_key_env_key, api_key.trim());\n }\n }\n\n let previous_config = existing_config.clone();\n let mut config = existing_config.unwrap_or_else(|| RuntimeConfig::default_for(data_dir));\n config.data_dir = data_dir.display().to_string();\n config.root_agent_id = root_agent_id.clone();\n config.model_profile = model_profile.clone();\n if profile.is_named() && previous_config.is_none() {\n config.sandbox.container_name = format!(\"remi-cat-sandbox-{}\", profile.label());\n }\n match sandbox_kind.as_str() {\n \"docker\" => {\n config.sandbox.kind = RuntimeSandboxKind::Docker;\n let default_host_dir = config.sandbox.host_dir_or_data_dir(&config.data_dir);\n config.sandbox.host_dir = prompt_with_default(\"Sandbox host dir\", &default_host_dir)?;\n config.sandbox.container_dir =\n prompt_with_default(\"Sandbox container dir\", &config.sandbox.container_dir)?;\n config.sandbox.image = prompt_with_default(\"Sandbox image\", &config.sandbox.image)?;\n config.sandbox.container_name =\n prompt_with_default(\"Sandbox container name\", &config.sandbox.container_name)?;\n config.sandbox.container_name =\n available_container_name(&config.sandbox.container_name, data_dir)?;\n }\n _ => {\n config.sandbox.kind = if sandbox_kind == \"disabled\" {\n RuntimeSandboxKind::Disabled\n } else {\n RuntimeSandboxKind::NoSandbox\n };\n config.sandbox.host_dir = data_dir.display().to_string();\n }\n }\n config.admin.enabled = prompt_bool_with_default(\"Admin enabled\", config.admin.enabled)?;\n if config.admin.enabled {\n config.admin.host = prompt_with_default(\"Admin listen host\", &config.admin.host)?;\n config.admin.port =\n prompt_with_default(\"Admin listen port\", &config.admin.port.to_string())?\n .parse()\n .context(\"invalid Admin listen port\")?;\n }\n config.im.transport = match feishu_transport.as_str() {\n \"event_hook\" | \"event-hook\" | \"hook\" => FeishuTransport::EventHook,\n _ => FeishuTransport::WebSocket,\n };\n config.im.mode = ImMode::Feishu;\n if matches!(config.im.transport, FeishuTransport::EventHook) {\n config.im.event_hook.host =\n prompt_with_default(\"Feishu Event Hook listen host\", &config.im.event_hook.host)?;\n config.im.event_hook.port = prompt_with_default(\n \"Feishu Event Hook listen port\",\n &config.im.event_hook.port.to_string(),\n )?\n .parse()\n .context(\"invalid Feishu Event Hook listen port\")?;\n config.im.event_hook.path =\n prompt_with_default(\"Feishu Event Hook path\", &config.im.event_hook.path)?;\n config.im.event_hook.verification_token = prompt_with_default(\n \"Feishu Event Hook verification token (blank to disable local token check)\",\n &config.im.event_hook.verification_token,\n )?;\n }\n\n let mut reserved_ports = configured_ports(data_dir)?;\n if config.admin.enabled {\n let requested = config.admin.port;\n config.admin.port = first_available_port(&config.admin.host, requested, &reserved_ports)?;\n print_port_adjustment(\"Admin\", requested, config.admin.port);\n reserved_ports.insert(config.admin.port);\n }\n if matches!(config.im.transport, FeishuTransport::EventHook) {\n let requested = config.im.event_hook.port;\n config.im.event_hook.port =\n first_available_port(&config.im.event_hook.host, requested, &reserved_ports)?;\n print_port_adjustment(\"Feishu Event Hook\", requested, config.im.event_hook.port);\n }\n\n let runtime_path = write_runtime_config(data_dir, &config)?;\n match run_setup_smoke(data_dir, &config, api_key_env_key, api_key.trim()).await {\n Ok(reply) => {\n let env_path = PathBuf::from(\".env\");\n if !profile.is_named() {\n upsert_dotenv_value(&env_path, \"REMI_DATA_DIR\", &config.data_dir)?;\n }\n if !api_key.trim().is_empty() {\n secret_store\n .lock()\n .await\n .set(api_key_env_key, api_key.trim())?;\n }\n println!(\"\\nSetup verification succeeded.\");\n println!(\"Smoke reply: {}\", reply.trim());\n println!(\"Saved runtime config to {}\", runtime_path.display());\n println!(\"\\nNext steps:\");\n println!(\n \"- Local chat: remi-cat{} cli --channel support --user alice --name Alice \\\"Hello\\\"\",\n profile\n .name\n .as_ref()\n .map(|name| format!(\" --profile {name}\"))\n .unwrap_or_default()\n );\n println!(\"- Feishu: run `remi-cat feishu init`, then run remi-cat\");\n println!(\"- ACP: configure ACP settings later when needed\");\n println!(\"- Shell: enable shell.mode only when you want local bash tools\");\n Ok(())\n }\n Err(err) => {\n if let Some(previous_config) = previous_config {\n let _ = write_runtime_config(data_dir, &previous_config);\n } else {\n let _ = std::fs::remove_file(&runtime_path);\n }\n anyhow::bail!(\"setup verification failed: {err:#}\")\n }\n }\n}\n\nasync fn run_setup_smoke(\n data_dir: &Path,\n config: &RuntimeConfig,\n api_key_env_key: &str,\n api_key: &str,\n) -> anyhow::Result {\n unsafe {\n std::env::set_var(\"REMI_DATA_DIR\", &config.data_dir);\n std::env::set_var(\"REMI_AGENT_ID\", &config.root_agent_id);\n std::env::set_var(\"REMI_MODEL_PROFILE\", &config.model_profile);\n std::env::set_var(\"REMI_SANDBOX_KIND\", config.sandbox.kind.as_env_value());\n std::env::set_var(\n \"REMI_SANDBOX_HOST_DIR\",\n config.sandbox.host_dir_or_data_dir(&config.data_dir),\n );\n std::env::set_var(\"REMI_SANDBOX_CONTAINER_DIR\", &config.sandbox.container_dir);\n std::env::set_var(\"REMI_SANDBOX_IMAGE\", &config.sandbox.image);\n std::env::set_var(\n \"REMI_SANDBOX_CONTAINER_NAME\",\n &config.sandbox.container_name,\n );\n if !api_key.trim().is_empty() {\n std::env::set_var(api_key_env_key, api_key.trim());\n }\n }\n let bot = Rc::new(CatBotBuilder::from_env()?.build()?);\n let session_id = \"setup-smoke\";\n let mut output = String::new();\n let opts = StreamOptions::default();\n let mut stream = std::pin::pin!(bot.stream_with_options(\n session_id,\n Content::text(format!(\n \"You are being verified for setup. Reply with one short sentence that says setup smoke passed for agent `{}`.\",\n config.root_agent_id\n )),\n opts\n ));\n while let Some(event) = stream.next().await {\n match event {\n CatEvent::Text(delta) => output.push_str(&delta),\n CatEvent::Error(err) => anyhow::bail!(err.to_string()),\n CatEvent::Done => break,\n _ => {}\n }\n }\n if output.trim().is_empty() {\n anyhow::bail!(\"model returned an empty reply\")\n }\n let _ = std::fs::remove_dir_all(data_dir.join(\"memory\").join(session_id));\n Ok(output)\n}\n\nfn prompt_with_default(label: &str, default: &str) -> anyhow::Result {\n print!(\"{label} [{default}]: \");\n io::stdout().flush()?;\n let mut input = String::new();\n io::stdin().read_line(&mut input)?;\n let value = input.trim();\n if value.is_empty() {\n Ok(default.to_string())\n } else {\n Ok(value.to_string())\n }\n}\n\nfn choose_from_list(label: &str, options: &[String], default_id: &str) -> anyhow::Result {\n println!(\"{label}:\");\n for option in options {\n println!(\" - {option}\");\n }\n prompt_with_default(\"Enter id\", default_id)\n}\n\nfn prompt_bool_with_default(label: &str, default: bool) -> anyhow::Result {\n let default_text = if default { \"yes\" } else { \"no\" };\n loop {\n let value = prompt_with_default(label, default_text)?;\n match value.trim().to_ascii_lowercase().as_str() {\n \"y\" | \"yes\" | \"true\" | \"1\" | \"on\" => return Ok(true),\n \"n\" | \"no\" | \"false\" | \"0\" | \"off\" => return Ok(false),\n _ => println!(\"Please enter yes or no.\"),\n }\n }\n}\n\nfn api_key_env_key_for_profile(profile: &ModelProfileConfig) -> &'static str {\n api_key_env_keys_for_profile(profile)\n .into_iter()\n .next()\n .unwrap_or(\"OPENAI_API_KEY\")\n}\n\nfn api_key_env_keys_for_profile(profile: &ModelProfileConfig) -> Vec<&'static str> {\n let provider = profile\n .provider\n .as_deref()\n .unwrap_or_default()\n .trim()\n .to_ascii_lowercase();\n let model = profile.model.to_ascii_lowercase();\n let base_url = profile\n .base_url\n .as_deref()\n .unwrap_or_default()\n .to_ascii_lowercase();\n\n let mut keys =\n if provider == \"mimo\" || model.contains(\"mimo\") || base_url.contains(\"xiaomimimo.com\") {\n vec![\"MIMO_API_KEY\"]\n } else if provider == \"kimi\"\n || provider == \"moonshot\"\n || model.contains(\"kimi\")\n || base_url.contains(\"moonshot.cn\")\n {\n vec![\"MOONSHOT_API_KEY\", \"KIMI_API_KEY\"]\n } else if provider == \"glm\"\n || provider == \"zhipu\"\n || provider == \"bigmodel\"\n || model.contains(\"glm\")\n || base_url.contains(\"bigmodel.cn\")\n {\n vec![\"GLM_API_KEY\", \"ZHIPU_API_KEY\", \"BIGMODEL_API_KEY\"]\n } else {\n Vec::new()\n };\n keys.extend([\"OPENAI_API_KEY\", \"REMI_API_KEY\"]);\n keys\n}\n\nfn prompt_secret(current: Option<&str>) -> anyhow::Result {\n let prompt = if current.is_some() {\n \"OpenAI-compatible API key [leave blank to keep current]: \"\n } else {\n \"OpenAI-compatible API key: \"\n };\n print!(\"{prompt}\");\n io::stdout().flush()?;\n let mut input = String::new();\n io::stdin().read_line(&mut input)?;\n let value = input.trim().to_string();\n if value.is_empty() {\n Ok(current.unwrap_or_default().to_string())\n } else {\n Ok(value)\n }\n}\n"}}}}