use anyhow::{Context, Result};
use colored::Colorize;
use std::io::{self, Write};
use std::path::PathBuf;
use crate::capture::watchers::aider::scan_directories_for_aider_files;
use crate::capture::watchers::{default_registry, Watcher, WatcherRegistry};
use crate::cli::commands::{completions, import};
use crate::config::Config;
use crate::daemon::DaemonState;
use crate::storage::db::default_db_path;
use crate::storage::{Database, Machine};
use crate::summarize::provider::{default_model, SummaryProviderKind};
use clap::CommandFactory;
#[derive(clap::Args)]
#[command(after_help = "EXAMPLES:\n \
lore init Run guided setup\n \
lore init --force Reinitialize even if already configured")]
pub struct Args {
#[arg(short, long)]
pub force: bool,
}
#[derive(Debug)]
struct DetectedTool {
name: String,
description: String,
has_sessions: bool,
session_count: usize,
}
pub fn run(args: Args) -> Result<()> {
println!("{}", "Lore Setup".bold().cyan());
println!("{}", "Reasoning history for code".dimmed());
println!();
let db_path = default_db_path()?;
let config_path = Config::config_path()?;
let already_initialized = db_path.exists() || config_path.exists();
if already_initialized && !args.force {
println!("{}", "Lore is already initialized.".yellow());
println!();
println!(" Database: {}", db_path.display());
println!(" Config: {}", config_path.display());
println!();
println!("Use {} to reconfigure.", "lore init --force".cyan());
return Ok(());
}
if already_initialized && args.force {
println!("{}", "Reinitializing Lore configuration...".yellow());
println!();
}
println!("{}", "Detecting installed AI coding tools...".bold());
let registry = default_registry();
let detected = detect_tools(®istry);
println!();
if detected.is_empty() {
println!(" {}", "No AI coding tools detected.".dimmed());
println!();
println!(
"Lore supports: Claude Code, Aider, Continue.dev, Cline, Codex, Gemini CLI, and more."
);
println!(
"Install one of these tools and run {} again.",
"lore init".cyan()
);
return Ok(());
}
let tools_with_sessions: Vec<&DetectedTool> =
detected.iter().filter(|t| t.has_sessions).collect();
let tools_without_sessions: Vec<&DetectedTool> =
detected.iter().filter(|t| !t.has_sessions).collect();
if !tools_with_sessions.is_empty() {
println!(" {} with existing sessions:", "Found".green());
for tool in &tools_with_sessions {
println!(
" {} - {} ({} session files)",
tool.name.cyan(),
tool.description,
tool.session_count
);
}
}
if !tools_without_sessions.is_empty() {
println!();
println!(" {} (no sessions yet):", "Available".dimmed());
for tool in &tools_without_sessions {
println!(" {} - {}", tool.name.cyan(), tool.description.dimmed());
}
}
println!();
let selected_watchers = prompt_watcher_selection(&detected)?;
if selected_watchers.is_empty() {
println!();
println!("{}", "No watchers selected. Setup cancelled.".yellow());
return Ok(());
}
println!();
println!("{}", "Creating configuration...".bold());
let mut config = Config {
watchers: selected_watchers.clone(),
..Config::default()
};
if let Some(parent) = config_path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("Failed to create config directory: {}", parent.display()))?;
}
println!();
println!("{}", "Machine Identity".bold());
let machine_id = config.get_or_create_machine_id()?;
println!(" ID: {}", machine_id.dimmed());
let detected_hostname = hostname::get()
.ok()
.and_then(|h| h.into_string().ok())
.unwrap_or_else(|| "unknown".to_string());
println!(" Detected hostname: {}", detected_hostname.cyan());
println!();
let prompt_text = format!(
"What would you like to call this machine? [{}]",
detected_hostname
);
print!("{}: ", prompt_text);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim();
let machine_name = if input.is_empty() {
detected_hostname
} else {
input.to_string()
};
config.set_machine_name(&machine_name)?;
println!("Machine name set to: {}", machine_name.green());
println!();
config
.save_to_path(&config_path)
.context("Failed to save configuration")?;
println!(" Created: {}", config_path.display());
let db_created = !db_path.exists();
let db = crate::storage::Database::open_default()?;
if db_created {
println!(" Created: {}", db_path.display());
}
let machine = Machine {
id: machine_id.clone(),
name: machine_name.clone(),
created_at: chrono::Utc::now().to_rfc3339(),
};
db.upsert_machine(&machine)?;
println!();
println!("Enabled watchers: {}", selected_watchers.join(", ").cyan());
let tools_with_sessions: Vec<_> = detected.iter().filter(|t| t.has_sessions).collect();
let total_sessions: usize = tools_with_sessions.iter().map(|t| t.session_count).sum();
let has_sessions = !tools_with_sessions.is_empty();
if has_sessions {
println!();
let prompt = format!(
"Import existing sessions now? ({} sessions from {} tools)",
total_sessions,
tools_with_sessions.len()
);
if prompt_yes_no(&prompt, true)? {
println!();
let stats = import::run_import(false, false)?;
println!();
println!(
"{}",
format!(
"Imported {} sessions from {} tools",
stats.imported, stats.tools_count
)
.bold()
);
if stats.skipped > 0 || stats.errors > 0 {
println!(" ({} skipped, {} errors)", stats.skipped, stats.errors);
}
}
}
let aider_enabled = selected_watchers.iter().any(|w| w == "aider");
if aider_enabled {
offer_aider_scan(&db)?;
}
println!();
offer_summary_setup(&mut config, &config_path)?;
println!();
offer_completions_install()?;
println!();
offer_service_install()?;
println!();
println!("{}", "Setup complete!".green().bold());
println!();
println!("Next steps:");
if !has_sessions {
println!(" {} - Import existing sessions", "lore import".cyan());
}
println!(" {} - Check current status", "lore status".cyan());
println!(" {} - View configuration", "lore config".cyan());
Ok(())
}
fn detect_tools(registry: &WatcherRegistry) -> Vec<DetectedTool> {
let mut detected = Vec::new();
for watcher in registry.all_watchers() {
let info = watcher.info();
let available = watcher.is_available();
if !available {
continue;
}
let sources = watcher.find_sources().unwrap_or_default();
let session_count = sources.len();
let has_sessions = !sources.is_empty();
detected.push(DetectedTool {
name: info.name.to_string(),
description: info.description.to_string(),
has_sessions,
session_count,
});
}
detected
}
fn prompt_watcher_selection(detected: &[DetectedTool]) -> Result<Vec<String>> {
let default_selection: Vec<String> = detected.iter().map(|t| t.name.clone()).collect();
println!("{}", "Which tools would you like to enable?".bold());
println!();
for (i, tool) in detected.iter().enumerate() {
let num = i + 1;
let status = if tool.has_sessions {
format!("({} sessions)", tool.session_count)
.green()
.to_string()
} else {
"(no sessions yet)".dimmed().to_string()
};
println!(
" [{}] {} - {} {}",
num,
tool.name.cyan(),
tool.description,
status
);
}
println!();
println!("Enter tool numbers separated by commas, or press Enter to enable all:");
print!("{}", "> ".cyan());
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim();
if input.is_empty() {
return Ok(default_selection);
}
let mut selected = Vec::new();
for part in input.split(',') {
let part = part.trim();
if let Ok(num) = part.parse::<usize>() {
if num >= 1 && num <= detected.len() {
selected.push(detected[num - 1].name.clone());
} else {
println!("{}: {} is not a valid option", "Warning".yellow(), num);
}
} else if !part.is_empty() {
if let Some(tool) = detected.iter().find(|t| t.name == part) {
selected.push(tool.name.clone());
} else {
println!(
"{}: '{}' is not a recognized tool",
"Warning".yellow(),
part
);
}
}
}
let mut seen = std::collections::HashSet::new();
selected.retain(|x| seen.insert(x.clone()));
Ok(selected)
}
fn prompt_yes_no(prompt: &str, default: bool) -> Result<bool> {
let hint = if default { "[Y/n]" } else { "[y/N]" };
print!("{prompt} {hint} ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim().to_lowercase();
if input.is_empty() {
return Ok(default);
}
match input.as_str() {
"y" | "yes" => Ok(true),
"n" | "no" => Ok(false),
_ => Ok(default),
}
}
fn offer_completions_install() -> Result<()> {
let shell = match completions::detect_shell() {
Some(s) => s,
None => {
println!(
"{}",
"Could not detect shell. Run 'lore completions install --shell <shell>' manually."
.dimmed()
);
return Ok(());
}
};
let shell_name = match shell {
clap_complete::Shell::Bash => "bash",
clap_complete::Shell::Zsh => "zsh",
clap_complete::Shell::Fish => "fish",
clap_complete::Shell::PowerShell => "PowerShell",
clap_complete::Shell::Elvish => "elvish",
_ => "shell",
};
let prompt = format!(
"Install shell completions for tab-completion? ({})",
shell_name
);
if !prompt_yes_no(&prompt, true)? {
return Ok(());
}
#[derive(clap::Parser)]
#[command(name = "lore")]
struct LoreCli {
#[command(subcommand)]
command: LoreCommand,
}
#[derive(clap::Subcommand)]
enum LoreCommand {
Init,
Status,
Sessions,
Show,
Link,
Unlink,
Delete,
Search,
Config,
Import,
Hooks,
Daemon,
Db,
Completions,
}
let mut cmd = LoreCli::command();
match completions::install_completions(&mut cmd, shell) {
Ok(path) => {
println!("Detected shell: {}", shell_name.cyan());
println!(
"Completions installed to: {}",
path.display().to_string().cyan()
);
let instructions = match shell {
clap_complete::Shell::Bash => {
format!("Restart your shell or run: source {}", path.display())
}
clap_complete::Shell::Zsh => {
"Restart your shell or run: autoload -Uz compinit && compinit".to_string()
}
clap_complete::Shell::Fish => {
format!("Restart your shell or run: source {}", path.display())
}
clap_complete::Shell::PowerShell => {
format!("Restart PowerShell or run: . {}", path.display())
}
clap_complete::Shell::Elvish => "Restart elvish or run: use lore".to_string(),
_ => "Restart your shell to activate completions.".to_string(),
};
println!("{}", instructions.dimmed());
}
Err(e) => {
println!(
"{}: {}",
"Warning".yellow(),
format!("Could not install completions: {}", e).dimmed()
);
println!(
"{}",
"Run 'lore completions install' manually later.".dimmed()
);
}
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum OperatingSystem {
MacOS,
Linux,
Windows,
Unknown,
}
impl std::fmt::Display for OperatingSystem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OperatingSystem::MacOS => write!(f, "macOS"),
OperatingSystem::Linux => write!(f, "Linux"),
OperatingSystem::Windows => write!(f, "Windows"),
OperatingSystem::Unknown => write!(f, "Unknown"),
}
}
}
pub fn detect_os() -> OperatingSystem {
#[cfg(target_os = "macos")]
{
OperatingSystem::MacOS
}
#[cfg(target_os = "linux")]
{
OperatingSystem::Linux
}
#[cfg(target_os = "windows")]
{
OperatingSystem::Windows
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
OperatingSystem::Unknown
}
}
fn offer_service_install() -> Result<()> {
let os = detect_os();
match os {
OperatingSystem::MacOS => offer_macos_service()?,
OperatingSystem::Linux => offer_linux_service()?,
OperatingSystem::Windows => {
println!(
"{}",
"Windows service management is not yet supported.".dimmed()
);
println!(
"{}",
"You can run 'lore daemon start' manually to start the daemon.".dimmed()
);
}
OperatingSystem::Unknown => {
println!(
"{}",
"Could not detect OS. You can run 'lore daemon start' manually.".dimmed()
);
}
}
Ok(())
}
fn offer_macos_service() -> Result<()> {
print_service_benefits();
if !prompt_yes_no("Start lore as a background service?", true)? {
print_service_declined_message(OperatingSystem::MacOS);
return Ok(());
}
let brew_check = std::process::Command::new("brew").arg("--version").output();
match brew_check {
Ok(output) if output.status.success() => {
println!("Starting lore service via Homebrew...");
let result = std::process::Command::new("brew")
.args(["services", "start", "lore"])
.output();
match result {
Ok(output) if output.status.success() => {
println!("{}", "Lore background service started!".green());
println!("{}", "Sessions will now be captured in real-time.".dimmed());
}
Ok(output) => {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("already started") {
println!("{}", "Lore background service is already running.".green());
println!("{}", "Sessions will now be captured in real-time.".dimmed());
} else {
println!(
"{}: {}",
"Warning".yellow(),
"Failed to start service via brew".dimmed()
);
if !stderr.is_empty() {
println!(" {}", stderr.trim().dimmed());
}
fallback_to_daemon_install()?;
}
}
Err(e) => {
println!(
"{}: {}",
"Warning".yellow(),
format!("Failed to run brew: {}", e).dimmed()
);
fallback_to_daemon_install()?;
}
}
}
_ => {
fallback_to_daemon_install()?;
}
}
Ok(())
}
fn fallback_to_daemon_install() -> Result<()> {
println!(
"{}",
"Falling back to native service installation...".dimmed()
);
let lore_exe = std::env::current_exe().context("Could not determine lore binary path")?;
let result = std::process::Command::new(&lore_exe)
.args(["daemon", "install"])
.output();
match result {
Ok(output) if output.status.success() => {
println!("{}", "Lore background service installed!".green());
println!("{}", "Sessions will now be captured in real-time.".dimmed());
}
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.contains("already installed") {
println!("{}", "Lore service is already installed.".green());
} else {
println!(
"{}",
"You can run 'lore daemon start' manually instead.".dimmed()
);
}
}
Err(e) => {
println!(
"{}: {}",
"Warning".yellow(),
format!("Failed to install service: {}", e).dimmed()
);
println!(
"{}",
"You can run 'lore daemon start' manually instead.".dimmed()
);
}
}
Ok(())
}
fn offer_linux_service() -> Result<()> {
print_service_benefits();
if !prompt_yes_no("Start lore as a background service?", true)? {
print_service_declined_message(OperatingSystem::Linux);
return Ok(());
}
let systemctl_check = std::process::Command::new("systemctl")
.arg("--version")
.output();
match systemctl_check {
Ok(output) if output.status.success() => {
if let Ok(state) = DaemonState::new() {
if state.is_running() {
if let Some(pid) = state.get_pid() {
println!(
"{}",
format!("Stopping existing daemon (PID {})...", pid).dimmed()
);
let _ = std::process::Command::new("kill")
.arg(pid.to_string())
.output();
std::thread::sleep(std::time::Duration::from_millis(500));
}
}
}
if let Err(e) = create_systemd_service_file() {
println!(
"{}: {}",
"Warning".yellow(),
format!("Failed to create service file: {}", e).dimmed()
);
println!(
"{}",
"You can run 'lore daemon start' manually instead.".dimmed()
);
return Ok(());
}
let reload = std::process::Command::new("systemctl")
.args(["--user", "daemon-reload"])
.output();
if let Err(e) = reload {
println!(
"{}: {}",
"Warning".yellow(),
format!("Failed to reload systemd: {}", e).dimmed()
);
}
let result = std::process::Command::new("systemctl")
.args(["--user", "enable", "--now", "lore"])
.output();
match result {
Ok(output) if output.status.success() => {
println!("{}", "Lore background service started!".green());
println!("{}", "Sessions will now be captured in real-time.".dimmed());
}
Ok(output) => {
let stderr = String::from_utf8_lossy(&output.stderr);
println!(
"{}: {}",
"Warning".yellow(),
"Failed to enable service".dimmed()
);
if !stderr.is_empty() {
println!(" {}", stderr.trim().dimmed());
}
println!(
"{}",
"You can run 'lore daemon start' manually instead.".dimmed()
);
}
Err(e) => {
println!(
"{}: {}",
"Warning".yellow(),
format!("Failed to run systemctl: {}", e).dimmed()
);
println!(
"{}",
"You can run 'lore daemon start' manually instead.".dimmed()
);
}
}
}
_ => {
println!("{}: {}", "Note".yellow(), "systemd not found".dimmed());
println!(
"{}",
"You can run 'lore daemon start' manually to start the daemon.".dimmed()
);
}
}
Ok(())
}
fn create_systemd_service_file() -> Result<()> {
let service_dir = dirs::config_dir()
.context("Could not find config directory")?
.join("systemd/user");
std::fs::create_dir_all(&service_dir).context("Failed to create systemd user directory")?;
let service_file = service_dir.join("lore.service");
let lore_binary = std::env::current_exe().context("Could not determine lore binary path")?;
let lore_binary_path = lore_binary.display();
let service_content = format!(
r#"[Unit]
Description=Lore - Reasoning history for code
Documentation=https://github.com/varalys/lore
[Service]
Type=simple
ExecStart={lore_binary_path} daemon start --foreground
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
"#
);
std::fs::write(&service_file, &service_content)
.with_context(|| format!("Failed to write service file: {}", service_file.display()))?;
println!(
"Created service file: {}",
service_file.display().to_string().dimmed()
);
Ok(())
}
fn print_service_benefits() {
println!("{}", "Background Service".bold());
println!();
println!("Lore can run as a background service to:");
println!(" - Capture sessions in real-time as you work");
println!(" - Auto-link sessions to commits when you commit");
println!(" - Track branch changes automatically");
println!();
}
fn print_service_declined_message(os: OperatingSystem) {
println!();
println!("You can start the service later with:");
match os {
OperatingSystem::MacOS => {
println!(" {}", "brew services start lore".cyan());
}
OperatingSystem::Linux => {
println!(" {}", "systemctl --user enable --now lore".cyan());
}
_ => {
println!(" {}", "lore daemon start".cyan());
}
}
}
#[allow(dead_code)]
pub fn cursor_data_path() -> PathBuf {
#[cfg(target_os = "macos")]
{
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("Library/Application Support/Cursor")
}
#[cfg(target_os = "linux")]
{
dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("Cursor")
}
#[cfg(target_os = "windows")]
{
dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("Cursor")
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("Cursor")
}
}
fn offer_aider_scan(db: &Database) -> Result<()> {
println!();
println!("{}", "Aider Projects".bold());
println!();
println!(
"{}",
"Aider stores chat history in project directories, not a central location.".dimmed()
);
println!(
"{}",
"Common folders (~/projects, ~/code, etc.) were already checked.".dimmed()
);
println!();
if !prompt_yes_no("Scan additional directories for aider projects?", false)? {
return Ok(());
}
let valid_dirs = loop {
println!();
println!("Enter directories to scan (comma-separated), or press Enter to skip:");
println!("{}", " Examples: ~/projects, ~/code, ~/work".dimmed());
print!("{}", "> ".cyan());
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim();
if input.is_empty() {
return Ok(());
}
let parts: Vec<&str> = input.split(',').map(|s| s.trim()).collect();
let directories: Vec<PathBuf> = parts
.into_iter()
.filter(|s| !s.is_empty())
.map(|s| {
if let Some(rest) = s.strip_prefix("~/") {
if let Some(home) = dirs::home_dir() {
home.join(rest)
} else {
PathBuf::from(s)
}
} else if s == "~" {
PathBuf::from("~") } else {
PathBuf::from(s)
}
})
.collect();
if directories.is_empty() {
println!("{}", "No directories specified.".yellow());
return Ok(());
}
let mut valid = Vec::new();
let mut invalid = Vec::new();
for d in directories {
if d.as_os_str() == "~" {
println!(
"{}",
"Scanning entire home directory is not recommended.".yellow()
);
invalid.push(d);
} else if d.exists() && d.is_dir() {
valid.push(d);
} else {
invalid.push(d);
}
}
if invalid.is_empty() {
break valid;
}
println!();
println!("{}", "Some directories were not found:".yellow());
for d in &invalid {
println!(" - {}", d.display());
}
if valid.is_empty() {
println!();
println!("Please re-enter directories (comma-separated), or press Enter to skip.");
continue;
}
println!();
println!(
"Found {} valid director{}:",
valid.len(),
if valid.len() == 1 { "y" } else { "ies" }
);
for d in &valid {
println!(" - {}", d.display());
}
println!();
if prompt_yes_no("Continue with these directories?", true)? {
break valid;
}
println!("Please re-enter directories (comma-separated), or press Enter to skip.");
};
println!();
println!("{}", "Scanning for aider projects...".bold());
let mut last_line_len = 0;
let found_files = scan_directories_for_aider_files(&valid_dirs, |current_dir, found_count| {
let dir_display = current_dir
.to_string_lossy()
.chars()
.take(60)
.collect::<String>();
let line = format!(
" {} {} [{}]",
"scanning".dimmed(),
dir_display,
format!("{} found", found_count).green()
);
print!("\r{:width$}\r", "", width = last_line_len);
print!("{}", line);
io::stdout().flush().ok();
last_line_len = line.len();
});
print!("\r{:width$}\r", "", width = last_line_len);
io::stdout().flush().ok();
if found_files.is_empty() {
println!(" {}", "No additional aider projects found.".dimmed());
return Ok(());
}
println!(
" Found {} aider project(s)",
found_files.len().to_string().green()
);
let watcher = crate::capture::watchers::aider::AiderWatcher;
let mut imported = 0;
let mut skipped = 0;
for file_path in &found_files {
match watcher.parse_source(file_path) {
Ok(sessions) => {
for (session, messages) in sessions {
if db.get_session(&session.id).ok().flatten().is_some() {
skipped += 1;
continue;
}
if let Err(e) = db.insert_session(&session) {
println!(" {}: Failed to import session: {}", "Warning".yellow(), e);
continue;
}
for message in &messages {
if let Err(e) = db.insert_message(message) {
println!(" {}: Failed to import message: {}", "Warning".yellow(), e);
}
}
imported += 1;
}
}
Err(e) => {
println!(
" {}: Failed to parse {}: {}",
"Warning".yellow(),
file_path.display(),
e
);
}
}
}
if imported > 0 {
println!(
" Imported {} aider session(s)",
imported.to_string().green()
);
}
if skipped > 0 {
println!(" ({} already imported)", skipped);
}
Ok(())
}
fn offer_summary_setup(config: &mut Config, config_path: &std::path::Path) -> Result<()> {
println!("{}", "Session Summaries".bold());
println!();
println!("Lore can generate short summaries of your AI coding sessions using an LLM.");
println!("Summaries help you quickly understand what each session accomplished.");
println!(
"{}",
"Requires an API key from a supported provider.".dimmed()
);
println!();
if !prompt_yes_no("Set up session summaries?", false)? {
return Ok(());
}
println!();
let provider = match prompt_provider_selection()? {
Some(p) => p,
None => return Ok(()),
};
println!();
let api_key = prompt_api_key(&provider)?;
if api_key.is_empty() {
println!("{}", "No API key entered. Summary setup skipped.".yellow());
return Ok(());
}
let kind: SummaryProviderKind = provider
.parse()
.map_err(|e: String| anyhow::anyhow!("{}", e))?;
let default = default_model(kind);
println!();
print!("Model [{}]: ", default.cyan());
io::stdout().flush()?;
let mut model_input = String::new();
io::stdin().read_line(&mut model_input)?;
let model_input = model_input.trim().to_string();
println!();
let auto_summarize = prompt_yes_no("Enable auto-summarize for new sessions?", true)?;
config.set("summary_provider", &provider)?;
config.set(&format!("summary_api_key_{}", provider), &api_key)?;
if !model_input.is_empty() {
config.set(&format!("summary_model_{}", provider), &model_input)?;
}
config.set(
"summary_auto",
if auto_summarize { "true" } else { "false" },
)?;
config.set("summary_auto_threshold", "4")?;
config
.save_to_path(config_path)
.context("Failed to save summary configuration")?;
let display_model = if model_input.is_empty() {
default.to_string()
} else {
model_input
};
println!();
println!("{}", "Summary configuration saved:".green());
println!(
" Provider: {}",
capitalize_provider(&provider).cyan()
);
println!(" Model: {}", display_model.cyan());
println!(" API key: {}", "(saved)".dimmed());
println!(
" Auto-summarize: {}",
if auto_summarize {
"enabled".green().to_string()
} else {
"disabled".dimmed().to_string()
}
);
Ok(())
}
fn prompt_provider_selection() -> Result<Option<String>> {
println!("{}", "Choose a provider:".bold());
println!(" [1] Anthropic (Claude)");
println!(" [2] OpenAI (GPT)");
println!(" [3] OpenRouter (multiple models)");
println!();
print!("Provider [1]: ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim();
let provider = match input {
"" | "1" => "anthropic",
"2" => "openai",
"3" => "openrouter",
_ => {
println!("{}: Invalid selection '{}'", "Warning".yellow(), input);
return Ok(None);
}
};
Ok(Some(provider.to_string()))
}
fn prompt_api_key(provider: &str) -> Result<String> {
let display_name = capitalize_provider(provider);
print!("API key for {} (hidden input): ", display_name);
io::stdout().flush()?;
let key = rpassword::read_password().context("Failed to read API key")?;
if !key.is_empty() {
let masked = if key.len() > 4 {
format!("...{}", &key[key.len() - 4..])
} else {
"****".to_string()
};
println!(" Key received: {}", masked.dimmed());
}
Ok(key)
}
fn capitalize_provider(provider: &str) -> &str {
match provider {
"anthropic" => "Anthropic",
"openai" => "OpenAI",
"openrouter" => "OpenRouter",
_ => provider,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_tools_returns_vec() {
let registry = default_registry();
let detected = detect_tools(®istry);
let _ = detected.len();
}
#[test]
fn test_detected_tool_has_required_fields() {
let tool = DetectedTool {
name: "test-tool".to_string(),
description: "A test tool".to_string(),
has_sessions: true,
session_count: 5,
};
assert_eq!(tool.name, "test-tool");
assert_eq!(tool.description, "A test tool");
assert!(tool.has_sessions);
assert_eq!(tool.session_count, 5);
}
#[test]
fn test_cursor_data_path_is_valid() {
let path = cursor_data_path();
assert!(path.to_string_lossy().contains("Cursor"));
}
#[test]
fn test_detect_os_returns_expected_type() {
let os = detect_os();
#[cfg(target_os = "macos")]
assert_eq!(
os,
OperatingSystem::MacOS,
"Expected MacOS on macOS platform"
);
#[cfg(target_os = "linux")]
assert_eq!(
os,
OperatingSystem::Linux,
"Expected Linux on Linux platform"
);
#[cfg(target_os = "windows")]
assert_eq!(
os,
OperatingSystem::Windows,
"Expected Windows on Windows platform"
);
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
assert_eq!(
os,
OperatingSystem::Unknown,
"Expected Unknown on unsupported platform"
);
}
#[test]
fn test_operating_system_display() {
assert_eq!(format!("{}", OperatingSystem::MacOS), "macOS");
assert_eq!(format!("{}", OperatingSystem::Linux), "Linux");
assert_eq!(format!("{}", OperatingSystem::Windows), "Windows");
assert_eq!(format!("{}", OperatingSystem::Unknown), "Unknown");
}
#[test]
fn test_operating_system_equality() {
assert_eq!(OperatingSystem::MacOS, OperatingSystem::MacOS);
assert_eq!(OperatingSystem::Linux, OperatingSystem::Linux);
assert_ne!(OperatingSystem::MacOS, OperatingSystem::Linux);
assert_ne!(OperatingSystem::Windows, OperatingSystem::Unknown);
}
#[test]
fn test_operating_system_clone() {
let os = OperatingSystem::MacOS;
let cloned = os;
assert_eq!(os, cloned);
}
}