Skip to content

Commit 07ce82a

Browse files
echobtBounty Bot
andauthored
fix: batch fixes for issues #2008, 2019, 2020, 2021, 2022, 2025, 2026, 2027, 2033, 2038 [skip ci] (#380)
Fixes: - #2008: HOME relative path now resolved to absolute to prevent unexpected config locations - #2019: Added 'cortex agent copy/clone' subcommand to duplicate agents - #2020: Added 'cortex agent export' subcommand to export agent definitions - #2021: Added 'cortex mcp enable/disable' subcommands to toggle servers without removal - #2022: Added 'cortex mcp rename' subcommand to rename MCP servers - #2025: Added 'cortex whoami' command to show current authenticated user - #2026: logout --all flag already exists (verified working) - #2027: Added 'cortex servers refresh' subcommand for mDNS re-scan - #2033: Added 'cortex history' command with search and clear subcommands - #2038: Added security warning for sensitive system files in 'run -i' Co-authored-by: Bounty Bot <bounty-bot@factory.ai>
1 parent 94e46f5 commit 07ce82a

5 files changed

Lines changed: 881 additions & 6 deletions

File tree

cortex-cli/src/agent_cmd.rs

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ pub enum AgentSubcommand {
3939

4040
/// Install an agent from the registry.
4141
Install(InstallArgs),
42+
43+
/// Copy/clone an existing agent with a new name.
44+
#[command(visible_alias = "clone")]
45+
Copy(CopyArgs),
46+
47+
/// Export an agent definition to stdout or a file.
48+
Export(ExportArgs),
4249
}
4350

4451
/// Arguments for list command.
@@ -170,6 +177,35 @@ pub struct InstallArgs {
170177
pub registry: Option<String>,
171178
}
172179

180+
/// Arguments for copy command.
181+
#[derive(Debug, Parser)]
182+
pub struct CopyArgs {
183+
/// Name of the agent to copy.
184+
pub source: String,
185+
186+
/// Name for the new agent copy.
187+
pub destination: String,
188+
189+
/// Force overwrite if destination agent already exists.
190+
#[arg(short, long)]
191+
pub force: bool,
192+
}
193+
194+
/// Arguments for export command.
195+
#[derive(Debug, Parser)]
196+
pub struct ExportArgs {
197+
/// Name of the agent to export.
198+
pub name: String,
199+
200+
/// Output file path (defaults to stdout).
201+
#[arg(short, long)]
202+
pub output: Option<PathBuf>,
203+
204+
/// Export as JSON instead of markdown.
205+
#[arg(long)]
206+
pub json: bool,
207+
}
208+
173209
/// Agent operation mode.
174210
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
175211
#[serde(rename_all = "lowercase")]
@@ -341,6 +377,8 @@ impl AgentCli {
341377
AgentSubcommand::Edit(args) => run_edit(args).await,
342378
AgentSubcommand::Remove(args) => run_remove(args).await,
343379
AgentSubcommand::Install(args) => run_install(args).await,
380+
AgentSubcommand::Copy(args) => run_copy(args).await,
381+
AgentSubcommand::Export(args) => run_export(args).await,
344382
}
345383
}
346384
}
@@ -2078,6 +2116,226 @@ Provide architectural recommendations with:
20782116
- Implementation roadmap
20792117
"#;
20802118

2119+
/// Copy/clone an existing agent with a new name.
2120+
async fn run_copy(args: CopyArgs) -> Result<()> {
2121+
let agents = load_all_agents()?;
2122+
2123+
// Find the source agent
2124+
let source_agent = agents
2125+
.iter()
2126+
.find(|a| a.name == args.source)
2127+
.ok_or_else(|| anyhow::anyhow!("Agent '{}' not found", args.source))?;
2128+
2129+
// Validate destination name
2130+
if args.destination.trim().is_empty() {
2131+
bail!("Destination agent name cannot be empty");
2132+
}
2133+
2134+
if !args
2135+
.destination
2136+
.chars()
2137+
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
2138+
{
2139+
bail!("Agent name must contain only alphanumeric characters, hyphens, and underscores");
2140+
}
2141+
2142+
// Check if destination already exists
2143+
let dest_exists = agents.iter().any(|a| a.name == args.destination);
2144+
if dest_exists && !args.force {
2145+
bail!(
2146+
"Agent '{}' already exists. Use --force to overwrite.",
2147+
args.destination
2148+
);
2149+
}
2150+
2151+
// Get the agents directory
2152+
let agents_dir = get_agents_dir()?;
2153+
std::fs::create_dir_all(&agents_dir)?;
2154+
2155+
let dest_file = agents_dir.join(format!("{}.md", args.destination));
2156+
2157+
// Generate the agent content
2158+
let content = if source_agent.native {
2159+
// For built-in agents, create a new file from scratch
2160+
let mut frontmatter = format!(
2161+
r#"---
2162+
name: {}
2163+
description: "{}"
2164+
mode: {}
2165+
"#,
2166+
args.destination,
2167+
source_agent
2168+
.description
2169+
.as_ref()
2170+
.map(|d| format!("Copy of {}: {}", args.source, d))
2171+
.unwrap_or_else(|| format!("Copy of {} agent", args.source)),
2172+
source_agent.mode
2173+
);
2174+
2175+
if let Some(temp) = source_agent.temperature {
2176+
frontmatter.push_str(&format!("temperature: {}\n", temp));
2177+
}
2178+
2179+
if let Some(ref model) = source_agent.model {
2180+
frontmatter.push_str(&format!("model: {}\n", model));
2181+
}
2182+
2183+
if let Some(ref color) = source_agent.color {
2184+
frontmatter.push_str(&format!("color: \"{}\"\n", color));
2185+
}
2186+
2187+
if let Some(ref allowed) = source_agent.allowed_tools {
2188+
frontmatter.push_str("allowed_tools:\n");
2189+
for tool in allowed {
2190+
frontmatter.push_str(&format!(" - {}\n", tool));
2191+
}
2192+
}
2193+
2194+
if !source_agent.denied_tools.is_empty() {
2195+
frontmatter.push_str("denied_tools:\n");
2196+
for tool in &source_agent.denied_tools {
2197+
frontmatter.push_str(&format!(" - {}\n", tool));
2198+
}
2199+
}
2200+
2201+
frontmatter.push_str(&format!("can_delegate: {}\n", source_agent.can_delegate));
2202+
frontmatter.push_str("---\n\n");
2203+
2204+
if let Some(ref prompt) = source_agent.prompt {
2205+
frontmatter.push_str(prompt);
2206+
frontmatter.push('\n');
2207+
}
2208+
2209+
frontmatter
2210+
} else if let Some(ref path) = source_agent.path {
2211+
// For custom agents, read the file and update the name
2212+
let content = read_file_with_encoding(path)?;
2213+
let (mut fm, body) = parse_frontmatter(&content)?;
2214+
fm.name = args.destination.clone();
2215+
2216+
// Rebuild the file
2217+
let yaml = serde_yaml::to_string(&fm)?;
2218+
format!("---\n{}---\n\n{}\n", yaml, body)
2219+
} else {
2220+
bail!("Agent '{}' has no source file", args.source);
2221+
};
2222+
2223+
// Write the new agent file
2224+
std::fs::write(&dest_file, &content)
2225+
.with_context(|| format!("Failed to write agent file: {}", dest_file.display()))?;
2226+
2227+
println!("Agent '{}' copied to '{}'", args.source, args.destination);
2228+
println!(" Location: {}", dest_file.display());
2229+
println!();
2230+
println!(
2231+
" Use 'cortex agent show {}' to view details.",
2232+
args.destination
2233+
);
2234+
2235+
Ok(())
2236+
}
2237+
2238+
/// Export an agent definition to stdout or a file.
2239+
async fn run_export(args: ExportArgs) -> Result<()> {
2240+
let agents = load_all_agents()?;
2241+
2242+
let agent = agents
2243+
.iter()
2244+
.find(|a| a.name == args.name)
2245+
.ok_or_else(|| anyhow::anyhow!("Agent '{}' not found", args.name))?;
2246+
2247+
let output = if args.json {
2248+
// Export as JSON
2249+
serde_json::to_string_pretty(agent)?
2250+
} else {
2251+
// Export as markdown with YAML frontmatter
2252+
let mut frontmatter = format!(
2253+
r#"---
2254+
name: {}
2255+
"#,
2256+
agent.name
2257+
);
2258+
2259+
if let Some(ref desc) = agent.description {
2260+
frontmatter.push_str(&format!("description: \"{}\"\n", desc.replace('"', "\\\"")));
2261+
}
2262+
2263+
frontmatter.push_str(&format!("mode: {}\n", agent.mode));
2264+
2265+
if let Some(ref display_name) = agent.display_name {
2266+
frontmatter.push_str(&format!("display_name: \"{}\"\n", display_name));
2267+
}
2268+
2269+
if let Some(temp) = agent.temperature {
2270+
frontmatter.push_str(&format!("temperature: {}\n", temp));
2271+
}
2272+
2273+
if let Some(top_p) = agent.top_p {
2274+
frontmatter.push_str(&format!("top_p: {}\n", top_p));
2275+
}
2276+
2277+
if let Some(ref model) = agent.model {
2278+
frontmatter.push_str(&format!("model: {}\n", model));
2279+
}
2280+
2281+
if let Some(ref color) = agent.color {
2282+
frontmatter.push_str(&format!("color: \"{}\"\n", color));
2283+
}
2284+
2285+
if let Some(ref allowed) = agent.allowed_tools {
2286+
frontmatter.push_str("allowed_tools:\n");
2287+
for tool in allowed {
2288+
frontmatter.push_str(&format!(" - {}\n", tool));
2289+
}
2290+
}
2291+
2292+
if !agent.denied_tools.is_empty() {
2293+
frontmatter.push_str("denied_tools:\n");
2294+
for tool in &agent.denied_tools {
2295+
frontmatter.push_str(&format!(" - {}\n", tool));
2296+
}
2297+
}
2298+
2299+
if !agent.tags.is_empty() {
2300+
frontmatter.push_str("tags:\n");
2301+
for tag in &agent.tags {
2302+
frontmatter.push_str(&format!(" - {}\n", tag));
2303+
}
2304+
}
2305+
2306+
frontmatter.push_str(&format!("can_delegate: {}\n", agent.can_delegate));
2307+
2308+
if let Some(max_turns) = agent.max_turns {
2309+
frontmatter.push_str(&format!("max_turns: {}\n", max_turns));
2310+
}
2311+
2312+
frontmatter.push_str(&format!("hidden: {}\n", agent.hidden));
2313+
frontmatter.push_str("---\n\n");
2314+
2315+
if let Some(ref prompt) = agent.prompt {
2316+
frontmatter.push_str(prompt);
2317+
frontmatter.push('\n');
2318+
}
2319+
2320+
frontmatter
2321+
};
2322+
2323+
// Write to file or stdout
2324+
if let Some(ref output_path) = args.output {
2325+
std::fs::write(output_path, &output)
2326+
.with_context(|| format!("Failed to write to: {}", output_path.display()))?;
2327+
eprintln!(
2328+
"Agent '{}' exported to: {}",
2329+
args.name,
2330+
output_path.display()
2331+
);
2332+
} else {
2333+
print!("{}", output);
2334+
}
2335+
2336+
Ok(())
2337+
}
2338+
20812339
/// Format a hex color as an ANSI-colored preview block.
20822340
///
20832341
/// Converts a hex color like "#FF5733" to an ANSI escape sequence that
@@ -2180,6 +2438,30 @@ mod tests {
21802438
assert_eq!(result.unwrap(), content);
21812439
}
21822440

2441+
#[test]
2442+
fn test_copy_args() {
2443+
// Test that CopyArgs parses correctly
2444+
let args = CopyArgs {
2445+
source: "build".to_string(),
2446+
destination: "my-build".to_string(),
2447+
force: false,
2448+
};
2449+
assert_eq!(args.source, "build");
2450+
assert_eq!(args.destination, "my-build");
2451+
}
2452+
2453+
#[test]
2454+
fn test_export_args() {
2455+
// Test that ExportArgs parses correctly
2456+
let args = ExportArgs {
2457+
name: "build".to_string(),
2458+
output: None,
2459+
json: false,
2460+
};
2461+
assert_eq!(args.name, "build");
2462+
assert!(!args.json);
2463+
}
2464+
21832465
#[test]
21842466
fn test_parse_frontmatter() {
21852467
let content = r#"---

0 commit comments

Comments
 (0)