use crate::app_config::AppType;
use crate::cli::i18n::texts;
use crate::cli::ui::info;
use crate::error::AppError;
use crate::provider::Provider;
use colored::Colorize;
use inquire::{Confirm, Select, Text};
use serde_json::{json, Value};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProviderAddMode {
Official,
ThirdParty,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn codex_official_settings_config_uses_upstream_seed_shape() {
let cfg = build_codex_official_settings_config(None).expect("build official settings");
assert!(
cfg.get("auth").is_some(),
"official Codex provider should carry auth like upstream snapshots"
);
assert_eq!(cfg.get("auth"), Some(&json!({})));
assert_eq!(cfg.get("config"), Some(&json!("")));
}
#[test]
fn codex_official_settings_config_preserves_auth_and_strips_provider_config() {
let cfg = build_codex_official_settings_config(Some(&json!({
"auth": {
"access_token": "oauth-token",
"refresh_token": "refresh-token"
},
"config": "model_provider = \"openai\"\nmodel = \"gpt-5.4\"\nmodel_reasoning_effort = \"high\"\n\n[model_providers.openai]\nbase_url = \"https://api.openai.com/v1\"\nwire_api = \"responses\"\nrequires_openai_auth = true\n"
})))
.expect("build official settings");
assert_eq!(
cfg.get("auth"),
Some(&json!({
"access_token": "oauth-token",
"refresh_token": "refresh-token"
}))
);
assert_eq!(
cfg.get("config").and_then(Value::as_str),
Some("model_reasoning_effort = \"high\"")
);
}
#[test]
fn build_codex_settings_config_defaults_model_to_gpt_5_4() {
let cfg = build_codex_settings_config(
Some("sk-test"),
"https://api.example.com/v1",
"",
"responses",
"custom",
);
let config = cfg
.get("config")
.and_then(Value::as_str)
.expect("config should be present");
assert!(config.contains("model = \"gpt-5.4\""));
assert!(config.contains("base_url = \"https://api.example.com/v1\""));
}
}
pub fn prompt_settings_config_for_add(
app_type: &AppType,
mode: ProviderAddMode,
) -> Result<Value, AppError> {
match (app_type, mode) {
(AppType::Claude, _) => prompt_claude_config(None),
(AppType::Codex, ProviderAddMode::Official) => prompt_codex_official_config(None),
(AppType::Codex, ProviderAddMode::ThirdParty) => prompt_codex_config(None),
(AppType::Gemini, _) => prompt_gemini_config(None),
(AppType::OpenCode, _) => Ok(json!({})),
(AppType::OpenClaw, _) => Ok(json!({})),
(AppType::Hermes, _) => Ok(json!({})),
}
}
fn clean_codex_provider_key(raw: &str) -> String {
crate::codex_config::clean_codex_provider_key(raw)
}
fn build_codex_settings_config(
api_key: Option<&str>,
base_url: &str,
model: &str,
wire_api: &str,
provider_key: &str,
) -> Value {
let model = if model.trim().is_empty() {
"gpt-5.4"
} else {
model.trim()
};
let base_url = base_url.trim();
let provider_key = clean_codex_provider_key(provider_key);
let config_toml = [
format!("model_provider = \"{}\"", provider_key),
format!("model = \"{}\"", model),
"model_reasoning_effort = \"high\"".to_string(),
"disable_response_storage = true".to_string(),
String::new(),
format!("[model_providers.{}]", provider_key),
format!("name = \"{}\"", provider_key),
format!("base_url = \"{}\"", base_url),
format!("wire_api = \"{}\"", wire_api),
"requires_openai_auth = true".to_string(),
String::new(),
]
.join("\n");
match api_key {
Some(key) => json!({
"auth": { "OPENAI_API_KEY": key.trim() },
"config": config_toml
}),
None => json!({
"config": config_toml
}),
}
}
fn build_codex_official_settings_config(current: Option<&Value>) -> Result<Value, AppError> {
let auth = current
.and_then(|value| value.get("auth"))
.and_then(Value::as_object)
.map(|value| Value::Object(value.clone()))
.unwrap_or_else(|| json!({}));
let config = current
.and_then(|value| value.get("config"))
.and_then(Value::as_str)
.unwrap_or("");
let cleaned_config = crate::codex_config::strip_codex_provider_config_text(config)?;
Ok(json!({
"auth": auth,
"config": cleaned_config
}))
}
#[derive(Default)]
pub struct OptionalFields {
pub notes: Option<String>,
pub icon: Option<String>,
pub icon_color: Option<String>,
pub sort_index: Option<usize>,
}
impl OptionalFields {
pub fn from_provider(provider: &Provider) -> Self {
Self {
notes: provider.notes.clone(),
icon: provider.icon.clone(),
icon_color: provider.icon_color.clone(),
sort_index: provider.sort_index,
}
}
}
pub fn generate_provider_id(name: &str, existing_ids: &[String]) -> String {
let base_id = name
.to_lowercase()
.chars()
.map(|c| {
if c.is_alphanumeric() || c == '-' || c == '_' {
c
} else if c.is_whitespace() {
'-'
} else {
'-'
}
})
.collect::<String>()
.trim_matches('-')
.to_string();
if !existing_ids.contains(&base_id) {
return base_id;
}
let mut counter = 1;
loop {
let candidate = format!("{}-{}", base_id, counter);
if !existing_ids.contains(&candidate) {
return candidate;
}
counter += 1;
}
}
pub fn prompt_basic_fields(
current: Option<&Provider>,
) -> Result<(String, Option<String>), AppError> {
let name = if let Some(provider) = current {
Text::new(texts::provider_name_label())
.with_initial_value(&provider.name)
.with_help_message(texts::provider_name_help())
.prompt()
.map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?
} else {
Text::new(texts::provider_name_label())
.with_placeholder("OpenAI")
.with_help_message(texts::provider_name_help())
.prompt()
.map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?
};
let name = name.trim().to_string();
if name.is_empty() {
return Err(AppError::InvalidInput(
texts::provider_name_empty_error().to_string(),
));
}
let website_url = if let Some(provider) = current {
let initial = provider.website_url.as_deref().unwrap_or("");
Text::new(texts::website_url_label())
.with_initial_value(initial)
.with_help_message(texts::website_url_help())
.prompt()
.map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?
} else {
Text::new(texts::website_url_label())
.with_placeholder("https://openai.com")
.with_help_message(texts::website_url_help())
.prompt()
.map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?
};
let website_url = if website_url.trim().is_empty() {
None
} else {
Some(website_url.trim().to_string())
};
Ok((name, website_url))
}
pub fn prompt_settings_config(
app_type: &AppType,
current: Option<&Value>,
codex_official: bool,
) -> Result<Value, AppError> {
match app_type {
AppType::Claude => prompt_claude_config(current),
AppType::Codex => {
if codex_official {
return prompt_codex_official_config(current);
}
let has_auth = current
.and_then(|v| v.get("auth"))
.and_then(|v| v.as_object())
.map(|obj| !obj.is_empty())
.unwrap_or(false);
let current_config_str = current
.and_then(|v| v.get("config"))
.and_then(|c| c.as_str());
let mut current_base_url: Option<String> = None;
if let Some(cfg) = current_config_str {
if let Ok(table) = toml::from_str::<toml::Table>(cfg) {
current_base_url = table
.get("base_url")
.and_then(|v| v.as_str())
.map(String::from);
if current_base_url.is_none() {
if let (Some(model_provider), Some(model_providers)) = (
table.get("model_provider").and_then(|v| v.as_str()),
table.get("model_providers").and_then(|v| v.as_table()),
) {
current_base_url = model_providers
.get(model_provider)
.and_then(|v| v.as_table())
.and_then(|t| t.get("base_url"))
.and_then(|v| v.as_str())
.map(String::from);
}
}
}
}
let is_openai_official_endpoint = current_base_url
.as_deref()
.map(|url| url.trim_start().starts_with("https://api.openai.com"))
.unwrap_or(false);
if !has_auth && is_openai_official_endpoint {
prompt_codex_official_config(current)
} else {
prompt_codex_config(current)
}
}
AppType::Gemini => prompt_gemini_config(current),
AppType::OpenCode => Ok(current.cloned().unwrap_or_else(|| json!({}))),
AppType::OpenClaw => Ok(current.cloned().unwrap_or_else(|| json!({}))),
AppType::Hermes => Ok(current.cloned().unwrap_or_else(|| json!({}))),
}
}
fn prompt_model_field(
field_name: &str,
env_key: &str,
placeholder: &str,
current: Option<&Value>,
) -> Result<Option<String>, AppError> {
let existing_value = current
.and_then(|v| v.get("env"))
.and_then(|e| e.get(env_key))
.and_then(|m| m.as_str());
let input = if let Some(existing) = existing_value {
Text::new(&format!("{}:", field_name))
.with_initial_value(existing)
.with_help_message(texts::model_default_help())
.prompt()
.map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?
} else {
Text::new(&format!("{}:", field_name))
.with_placeholder(placeholder)
.with_help_message(texts::model_default_help())
.prompt()
.map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?
};
let trimmed = input.trim();
if trimmed.is_empty() {
if existing_value.is_some() {
Ok(None)
} else {
Ok(None)
}
} else {
Ok(Some(trimmed.to_string()))
}
}
fn prompt_claude_config(current: Option<&Value>) -> Result<Value, AppError> {
println!("\n{}", texts::config_claude_header().bright_cyan().bold());
let api_key = if let Some(current_key) = current
.and_then(|v| v.get("env"))
.and_then(|e| e.get("ANTHROPIC_AUTH_TOKEN"))
.and_then(|k| k.as_str())
.filter(|s| !s.is_empty())
{
Text::new(texts::api_key_label())
.with_initial_value(current_key)
.with_help_message(texts::api_key_help())
.prompt()
.map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?
} else {
Text::new(texts::api_key_label())
.with_placeholder("sk-ant-...")
.with_help_message(texts::api_key_help())
.prompt()
.map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?
};
let base_url = if let Some(current_url) = current
.and_then(|v| v.get("env"))
.and_then(|e| e.get("ANTHROPIC_BASE_URL"))
.and_then(|u| u.as_str())
.filter(|s| !s.is_empty())
{
Text::new(texts::base_url_label())
.with_initial_value(current_url)
.with_help_message(texts::api_key_help())
.prompt()
.map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?
} else {
Text::new(texts::base_url_label())
.with_placeholder(texts::base_url_placeholder())
.with_help_message(texts::api_key_help())
.prompt()
.map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?
};
let config_models = Confirm::new(texts::configure_model_names_prompt())
.with_default(false)
.with_help_message(texts::api_key_help())
.prompt()
.map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?;
let mut env = serde_json::Map::new();
env.insert("ANTHROPIC_AUTH_TOKEN".to_string(), json!(api_key.trim()));
env.insert("ANTHROPIC_BASE_URL".to_string(), json!(base_url.trim()));
if config_models {
let model = prompt_model_field(
texts::model_default_label(),
"ANTHROPIC_MODEL",
texts::model_sonnet_placeholder(),
current,
)?;
let haiku = prompt_model_field(
texts::model_haiku_label(),
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
texts::model_haiku_placeholder(),
current,
)?;
let sonnet = prompt_model_field(
texts::model_sonnet_label(),
"ANTHROPIC_DEFAULT_SONNET_MODEL",
texts::model_sonnet_placeholder(),
current,
)?;
let opus = prompt_model_field(
texts::model_opus_label(),
"ANTHROPIC_DEFAULT_OPUS_MODEL",
texts::model_opus_placeholder(),
current,
)?;
if let Some(value) = model {
env.insert("ANTHROPIC_MODEL".to_string(), json!(value));
}
if let Some(value) = haiku {
env.insert("ANTHROPIC_DEFAULT_HAIKU_MODEL".to_string(), json!(value));
}
if let Some(value) = sonnet {
env.insert("ANTHROPIC_DEFAULT_SONNET_MODEL".to_string(), json!(value));
}
if let Some(value) = opus {
env.insert("ANTHROPIC_DEFAULT_OPUS_MODEL".to_string(), json!(value));
}
}
Ok(json!({ "env": env }))
}
fn prompt_codex_config(current: Option<&Value>) -> Result<Value, AppError> {
println!("\n{}", texts::config_codex_header().bright_cyan().bold());
let current_api_key = current
.and_then(|v| v.get("auth"))
.and_then(|a| a.get("OPENAI_API_KEY"))
.and_then(|k| k.as_str())
.filter(|s| !s.is_empty());
let current_config_str = current
.and_then(|v| v.get("config"))
.and_then(|c| c.as_str());
let mut current_base_url: Option<String> = None;
let mut current_model: Option<String> = None;
if let Some(cfg) = current_config_str {
if let Ok(table) = toml::from_str::<toml::Table>(cfg) {
current_base_url = table
.get("base_url")
.and_then(|v| v.as_str())
.map(String::from);
if current_base_url.is_none() {
if let (Some(model_provider), Some(model_providers)) = (
table.get("model_provider").and_then(|v| v.as_str()),
table.get("model_providers").and_then(|v| v.as_table()),
) {
current_base_url = model_providers
.get(model_provider)
.and_then(|v| v.as_table())
.and_then(|t| t.get("base_url"))
.and_then(|v| v.as_str())
.map(String::from);
}
}
current_model = table
.get("model")
.and_then(|v| v.as_str())
.map(String::from);
}
}
let api_key = if let Some(current_key) = current_api_key {
Text::new(texts::openai_api_key_label())
.with_initial_value(current_key)
.with_help_message(texts::api_key_help())
.prompt()
.map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?
} else {
Text::new(texts::openai_api_key_label())
.with_placeholder("sk-...")
.with_help_message(texts::api_key_help())
.prompt()
.map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?
};
let base_url = if let Some(current) = current_base_url.as_deref() {
Text::new(&format!("{}:", texts::tui_label_base_url()))
.with_initial_value(current)
.with_help_message("API endpoint (e.g., https://api.openai.com/v1)")
.prompt()
.map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?
} else {
Text::new(&format!("{}:", texts::tui_label_base_url()))
.with_placeholder("https://api.openai.com/v1")
.with_help_message("API endpoint")
.prompt()
.map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?
};
let base_url = base_url.trim().to_string();
if base_url.is_empty() {
return Err(AppError::InvalidInput(
texts::base_url_empty_error().to_string(),
));
}
let model = if let Some(current) = current_model.as_deref() {
Text::new(&format!("{}:", texts::model_label()))
.with_initial_value(current)
.with_help_message("Model name (e.g., gpt-5.4, o3)")
.prompt()
.map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?
} else {
Text::new(&format!("{}:", texts::model_label()))
.with_placeholder("gpt-5.4")
.with_help_message("Model name")
.prompt()
.map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?
};
Ok(build_codex_settings_config(
Some(api_key.trim()),
&base_url,
model.trim(),
"responses",
"custom",
))
}
fn prompt_codex_official_config(current: Option<&Value>) -> Result<Value, AppError> {
println!("\n{}", texts::config_codex_header().bright_cyan().bold());
println!(
"{}",
info("OpenAI Official keeps the stored auth snapshot and uses the upstream empty official config.")
);
build_codex_official_settings_config(current)
}
fn prompt_gemini_config(current: Option<&Value>) -> Result<Value, AppError> {
println!("\n{}", texts::config_gemini_header().bright_cyan().bold());
let current_auth_type = detect_gemini_auth_type(current);
let default_index = match current_auth_type.as_deref() {
Some("oauth") => 0,
_ => 1, };
let auth_options = vec![texts::google_oauth_official(), texts::generic_api_key()];
let auth_type = Select::new(texts::auth_type_label(), auth_options.clone())
.with_starting_cursor(default_index)
.with_help_message(texts::select_auth_method_help())
.prompt()
.map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?;
let google_oauth = texts::google_oauth_official();
if auth_type == google_oauth {
println!("{}", texts::use_google_oauth_warning().yellow());
Ok(json!({
"env": {},
"config": {}
}))
} else {
let api_key = if let Some(current_key) = current
.and_then(|v| v.get("env"))
.and_then(|e| e.get("GEMINI_API_KEY"))
.and_then(|k| k.as_str())
.filter(|s| !s.is_empty())
{
Text::new(texts::gemini_api_key_label())
.with_initial_value(current_key)
.with_help_message(texts::generic_api_key_help())
.prompt()
.map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?
} else {
Text::new(texts::gemini_api_key_label())
.with_placeholder("AIza... or pk-...")
.with_help_message(texts::generic_api_key_help())
.prompt()
.map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?
};
let base_url = if let Some(current_url) = current
.and_then(|v| v.get("env"))
.and_then(|e| e.get("GOOGLE_GEMINI_BASE_URL"))
.and_then(|u| u.as_str())
.filter(|s| !s.is_empty())
{
Text::new(texts::gemini_base_url_label())
.with_initial_value(current_url)
.with_help_message(texts::gemini_base_url_help())
.prompt()
.map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?
} else {
Text::new(texts::gemini_base_url_label())
.with_placeholder(texts::gemini_base_url_placeholder())
.with_help_message(texts::gemini_base_url_help())
.prompt()
.map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?
};
Ok(json!({
"env": {
"GEMINI_API_KEY": api_key.trim(),
"GOOGLE_GEMINI_BASE_URL": base_url.trim()
},
"config": {}
}))
}
}
pub fn prompt_optional_fields(current: Option<&Provider>) -> Result<OptionalFields, AppError> {
println!("\n{}", texts::optional_fields_config().bright_cyan().bold());
let notes = if let Some(provider) = current {
let initial = provider.notes.as_deref().unwrap_or("");
Text::new(texts::notes_label())
.with_initial_value(initial)
.with_help_message(texts::notes_help_edit())
.prompt()
.map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?
} else {
Text::new(texts::notes_label())
.with_placeholder(texts::notes_example_placeholder())
.with_help_message(texts::notes_help_new())
.prompt()
.map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?
};
let notes = if notes.trim().is_empty() {
None
} else {
Some(notes.trim().to_string())
};
let sort_index_str = if let Some(provider) = current {
let initial = provider
.sort_index
.map(|i| i.to_string())
.unwrap_or_default();
Text::new(texts::sort_index_label())
.with_initial_value(&initial)
.with_help_message(texts::sort_index_help_edit())
.prompt()
.map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?
} else {
Text::new(texts::sort_index_label())
.with_placeholder(texts::sort_index_placeholder())
.with_help_message(texts::sort_index_help_new())
.prompt()
.map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?
};
let sort_index =
if sort_index_str.trim().is_empty() {
None
} else {
Some(sort_index_str.trim().parse::<usize>().map_err(|_| {
AppError::InvalidInput(texts::invalid_sort_index_number().to_string())
})?)
};
Ok(OptionalFields {
notes,
icon: None,
icon_color: None,
sort_index,
})
}
pub fn display_provider_summary(provider: &Provider, app_type: &AppType) {
println!(
"\n{}",
texts::provider_config_summary().bright_green().bold()
);
println!("{}: {}", texts::id_label().bright_yellow(), provider.id);
println!(
"{}: {}",
texts::provider_name_label().bright_yellow(),
provider.name
);
if let Some(website) = &provider.website_url {
println!("{}: {}", texts::website_label().bright_yellow(), website);
}
println!("\n{}", texts::core_config_label().bright_cyan());
match app_type {
AppType::Claude => {
if let Some(env) = provider.settings_config.get("env") {
if let Some(api_key) = env.get("ANTHROPIC_AUTH_TOKEN").and_then(|v| v.as_str()) {
println!(
" {}: {}",
texts::api_key_display_label(),
mask_api_key(api_key)
);
}
if let Some(base_url) = env.get("ANTHROPIC_BASE_URL").and_then(|v| v.as_str()) {
println!(" {}: {}", texts::base_url_display_label(), base_url);
}
if let Some(model) = env.get("ANTHROPIC_MODEL").and_then(|v| v.as_str()) {
println!(" {}: {}", texts::model_label(), model);
}
}
}
AppType::Codex => {
if let Some(auth) = provider.settings_config.get("auth") {
if let Some(api_key) = auth.get("OPENAI_API_KEY").and_then(|v| v.as_str()) {
println!(
" {}: {}",
texts::api_key_display_label(),
mask_api_key(api_key)
);
}
}
if let Some(config) = provider
.settings_config
.get("config")
.and_then(|v| v.as_str())
{
println!(" {}", texts::config_toml_lines(config.lines().count()));
}
}
AppType::Gemini => {
if let Some(env) = provider.settings_config.get("env") {
if let Some(api_key) = env.get("GEMINI_API_KEY").and_then(|v| v.as_str()) {
println!(
" {}: {}",
texts::api_key_display_label(),
mask_api_key(api_key)
);
}
if let Some(base_url) = env
.get("GOOGLE_GEMINI_BASE_URL")
.or_else(|| env.get("BASE_URL"))
.and_then(|v| v.as_str())
{
println!(" {}: {}", texts::base_url_display_label(), base_url);
}
}
}
AppType::OpenCode => {
if let Some(options) = provider.settings_config.get("options") {
if let Some(api_key) = options.get("apiKey").and_then(|v| v.as_str()) {
println!(
" {}: {}",
texts::api_key_display_label(),
mask_api_key(api_key)
);
}
if let Some(base_url) = options.get("baseURL").and_then(|v| v.as_str()) {
println!(" {}: {}", texts::base_url_display_label(), base_url);
}
}
if let Some(models) = provider
.settings_config
.get("models")
.and_then(|v| v.as_object())
{
println!(" {}: {}", texts::model_label(), models.len());
}
}
AppType::OpenClaw => {
if let Some(api_key) = provider
.settings_config
.get("apiKey")
.and_then(|v| v.as_str())
{
println!(
" {}: {}",
texts::api_key_display_label(),
mask_api_key(api_key)
);
}
if let Some(base_url) = provider
.settings_config
.get("baseUrl")
.and_then(|v| v.as_str())
{
println!(" {}: {}", texts::base_url_display_label(), base_url);
}
if let Some(models) = provider
.settings_config
.get("models")
.and_then(|v| v.as_array())
{
println!(" {}: {}", texts::model_label(), models.len());
}
}
AppType::Hermes => {
if let Some(api_key) = provider
.settings_config
.get("apiKey")
.and_then(|v| v.as_str())
{
println!(
" {}: {}",
texts::api_key_display_label(),
mask_api_key(api_key)
);
}
if let Some(base_url) = provider
.settings_config
.get("baseUrl")
.and_then(|v| v.as_str())
{
println!(" {}: {}", texts::base_url_display_label(), base_url);
}
if let Some(models) = provider
.settings_config
.get("models")
.and_then(|v| v.as_array())
{
println!(" {}: {}", texts::model_label(), models.len());
}
}
}
if provider.notes.is_some() || provider.sort_index.is_some() {
println!("\n{}", texts::optional_fields_label().bright_cyan());
if let Some(notes) = &provider.notes {
println!(" {}: {}", texts::notes_label_colon(), notes);
}
if let Some(idx) = provider.sort_index {
println!(" {}: {}", texts::sort_index_label_colon(), idx);
}
}
println!("{}", texts::summary_divider().bright_green().bold());
}
pub fn current_timestamp() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64
}
fn detect_gemini_auth_type(value: Option<&Value>) -> Option<String> {
if let Some(env) = value.and_then(|v| v.get("env")) {
if env.get("GEMINI_API_KEY").is_some() {
if env
.get("GOOGLE_GEMINI_BASE_URL")
.and_then(|v| v.as_str())
.map(|s| s.contains("packycode"))
.unwrap_or(false)
{
return Some("packycode".to_string());
} else {
return Some("generic".to_string());
}
}
}
if value
.and_then(|v| v.get("env"))
.map(|v| v.as_object().map(|o| o.is_empty()).unwrap_or(true))
.unwrap_or(true)
{
return Some("oauth".to_string());
}
None
}
fn mask_api_key(key: &str) -> String {
if key.len() <= 8 {
return "***".to_string();
}
format!("{}...{}", &key[..4], &key[key.len() - 4..])
}