From fc1cb77f0572948552377b23f7754a3016ac7dfa Mon Sep 17 00:00:00 2001 From: Can Celik Date: Sun, 13 Sep 2026 18:36:47 +0300 Subject: feat: add Letta Code detection and restore (#3107) refs #3106 Co-authored-by: Cameron --- src/cli/integration.rs | 128 ++++++++++++++++++++++++++++++++----------------- src/cli/spec.rs | 19 ++++++-- 2 files changed, 99 insertions(+), 48 deletions(-) (limited to 'src/cli') diff --git a/src/cli/integration.rs b/src/cli/integration.rs index d0a53f7b..a8d5a5bf 100644 --- a/src/cli/integration.rs +++ b/src/cli/integration.rs @@ -38,38 +38,63 @@ fn integration_status(args: &[String]) -> std::io::Result { for status in crate::integration::installed_integration_statuses() { let target = crate::integration::integration_target_label(status.target); - let version = match status.installed_version { - Some(version) => format!("v{version}"), - None => "legacy".to_string(), - }; - let state = match status.state { - crate::integration::IntegrationStatusKind::NotInstalled => "not installed".to_string(), - crate::integration::IntegrationStatusKind::Current => { - format!("current ({version})") - } - crate::integration::IntegrationStatusKind::Outdated - if status - .installed_version - .is_some_and(|installed| installed >= status.expected_version) => - { - format!("needs repair ({version})") - } - crate::integration::IntegrationStatusKind::Outdated => { - format!("outdated ({version} < v{})", status.expected_version) - } - }; + let state = describe_integration_state( + status.state, + status.installed_version, + status.expected_version, + ); println!("{target}: {state} ({})", status.path.display()); } + if let Some(status) = crate::integration::experimental_letta_integration_status() { + let state = describe_integration_state( + status.state, + status.installed_version, + status.expected_version, + ); + println!( + "{} (experimental): {state} ({})", + status.label, + status.path.display() + ); + } + Ok(0) } +fn describe_integration_state( + state: crate::integration::IntegrationStatusKind, + installed_version: Option, + expected_version: u32, +) -> String { + let version = match installed_version { + Some(version) => format!("v{version}"), + None => "legacy".to_string(), + }; + match state { + crate::integration::IntegrationStatusKind::NotInstalled => "not installed".to_string(), + crate::integration::IntegrationStatusKind::Current => format!("current ({version})"), + crate::integration::IntegrationStatusKind::Outdated + if installed_version.is_some_and(|installed| installed >= expected_version) => + { + format!("needs repair ({version})") + } + crate::integration::IntegrationStatusKind::Outdated => { + format!("outdated ({version} < v{expected_version})") + } + } +} + fn integration_install(args: &[String]) -> std::io::Result { let Some(target) = parse_integration_target(args, "install")? else { return Ok(2); }; - match crate::integration::install_target(target) { + let installed = match target { + IntegrationCommandTarget::Builtin(target) => crate::integration::install_target(target), + IntegrationCommandTarget::Letta => crate::integration::install_experimental_letta(), + }; + match installed { Ok(messages) => { print_integration_messages(messages); Ok(0) @@ -86,7 +111,11 @@ fn integration_uninstall(args: &[String]) -> std::io::Result { return Ok(2); }; - match crate::integration::uninstall_target(target) { + let removed = match target { + IntegrationCommandTarget::Builtin(target) => crate::integration::uninstall_target(target), + IntegrationCommandTarget::Letta => crate::integration::uninstall_experimental_letta(), + }; + match removed { Ok(messages) => { print_integration_messages(messages); Ok(0) @@ -104,45 +133,56 @@ fn print_integration_messages(messages: Vec) { } } +/// Integration target accepted by the CLI. Letta is deliberately kept out of +/// the frozen client endpoint `IntegrationTarget` enum and is handled as an +/// experimental CLI-only target until the agent registry replaces it. +enum IntegrationCommandTarget { + Builtin(IntegrationTarget), + Letta, +} + fn parse_integration_target( args: &[String], action: &str, -) -> std::io::Result> { +) -> std::io::Result> { let Some(target) = args.first().map(|arg| arg.as_str()) else { eprintln!( - "usage: herdr integration {action} " + "usage: herdr integration {action} " ); return Ok(None); }; if args.len() != 1 { eprintln!( - "usage: herdr integration {action} " + "usage: herdr integration {action} " ); return Ok(None); } let parsed = match target { - "pi" => IntegrationTarget::Pi, - "omp" => IntegrationTarget::Omp, - "claude" => IntegrationTarget::Claude, - "codex" => IntegrationTarget::Codex, - "copilot" => IntegrationTarget::Copilot, - "devin" => IntegrationTarget::Devin, - "droid" => IntegrationTarget::Droid, - "kimi" => IntegrationTarget::Kimi, - "opencode" => IntegrationTarget::Opencode, - "kilo" => IntegrationTarget::Kilo, - "hermes" => IntegrationTarget::Hermes, - "qodercli" => IntegrationTarget::Qodercli, - "qwen" => IntegrationTarget::Qwen, - "cursor" => IntegrationTarget::Cursor, - "mastracode" => IntegrationTarget::Mastracode, - "antigravity-cli" | "antigravity_cli" => IntegrationTarget::AntigravityCli, - "grok" => IntegrationTarget::Grok, + "pi" => IntegrationCommandTarget::Builtin(IntegrationTarget::Pi), + "omp" => IntegrationCommandTarget::Builtin(IntegrationTarget::Omp), + "claude" => IntegrationCommandTarget::Builtin(IntegrationTarget::Claude), + "codex" => IntegrationCommandTarget::Builtin(IntegrationTarget::Codex), + "copilot" => IntegrationCommandTarget::Builtin(IntegrationTarget::Copilot), + "devin" => IntegrationCommandTarget::Builtin(IntegrationTarget::Devin), + "droid" => IntegrationCommandTarget::Builtin(IntegrationTarget::Droid), + "kimi" => IntegrationCommandTarget::Builtin(IntegrationTarget::Kimi), + "opencode" => IntegrationCommandTarget::Builtin(IntegrationTarget::Opencode), + "kilo" => IntegrationCommandTarget::Builtin(IntegrationTarget::Kilo), + "hermes" => IntegrationCommandTarget::Builtin(IntegrationTarget::Hermes), + "qodercli" => IntegrationCommandTarget::Builtin(IntegrationTarget::Qodercli), + "qwen" => IntegrationCommandTarget::Builtin(IntegrationTarget::Qwen), + "letta" => IntegrationCommandTarget::Letta, + "cursor" => IntegrationCommandTarget::Builtin(IntegrationTarget::Cursor), + "mastracode" => IntegrationCommandTarget::Builtin(IntegrationTarget::Mastracode), + "antigravity-cli" | "antigravity_cli" => { + IntegrationCommandTarget::Builtin(IntegrationTarget::AntigravityCli) + } + "grok" => IntegrationCommandTarget::Builtin(IntegrationTarget::Grok), _ => { eprintln!("unknown integration target: {target}"); eprintln!( - "currently supported: pi, omp, claude, codex, copilot, devin, droid, kimi, opencode, kilo, hermes, qodercli, qwen, cursor, mastracode, antigravity-cli, grok" + "currently supported: pi, omp, claude, codex, copilot, devin, droid, kimi, opencode, kilo, hermes, qodercli, qwen, letta, cursor, mastracode, antigravity-cli, grok" ); return Ok(None); } @@ -166,6 +206,7 @@ fn print_integration_help() { eprintln!(" herdr integration install hermes"); eprintln!(" herdr integration install qodercli"); eprintln!(" herdr integration install qwen"); + eprintln!(" herdr integration install letta"); eprintln!(" herdr integration install cursor"); eprintln!(" herdr integration install mastracode"); eprintln!(" herdr integration install antigravity-cli"); @@ -183,6 +224,7 @@ fn print_integration_help() { eprintln!(" herdr integration uninstall hermes"); eprintln!(" herdr integration uninstall qodercli"); eprintln!(" herdr integration uninstall qwen"); + eprintln!(" herdr integration uninstall letta"); eprintln!(" herdr integration uninstall cursor"); eprintln!(" herdr integration uninstall mastracode"); eprintln!(" herdr integration uninstall antigravity-cli"); diff --git a/src/cli/spec.rs b/src/cli/spec.rs index 0d8a58ea..9465dcc3 100644 --- a/src/cli/spec.rs +++ b/src/cli/spec.rs @@ -895,10 +895,12 @@ fn integration_target_arg() -> Arg { } fn integration_target_values() -> Vec<&'static str> { - crate::api::schema::IntegrationTarget::ALL + let mut values: Vec<&'static str> = crate::api::schema::IntegrationTarget::ALL .into_iter() .map(crate::integration::integration_target_label) - .collect() + .collect(); + values.extend_from_slice(crate::integration::EXPERIMENTAL_INTEGRATION_TARGET_LABELS); + values } fn id_command(name: &'static str, id: &'static str, about: &'static str) -> Command { @@ -1121,6 +1123,15 @@ mod tests { fn spec_matches_all_integration_targets() { let cmd = super::command(); let install = command_path(&cmd, &["integration", "install"]); + let mut expected: Vec = crate::api::schema::IntegrationTarget::ALL + .map(crate::integration::integration_target_label) + .map(str::to_string) + .to_vec(); + expected.extend( + crate::integration::EXPERIMENTAL_INTEGRATION_TARGET_LABELS + .iter() + .map(|label| (*label).to_string()), + ); assert_eq!( argument(install, "target") .get_value_parser() @@ -1128,9 +1139,7 @@ mod tests { .unwrap() .map(|value| value.get_name().to_string()) .collect::>(), - crate::api::schema::IntegrationTarget::ALL - .map(crate::integration::integration_target_label) - .map(str::to_string) + expected ); } -- cgit v1.2.3