@@ -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