use crate::agents::builder::AgentPresetBuildOutput;
use crate::agents::manifest::AgentManifestV1;
use crate::config::resolve_config_dir;
use crate::fs_utils::atomic_write_with_mode;
use crate::path_utils::validate_path_segment;
use crate::skills::catalog::scan::discover_skills;
use crate::skills::catalog::types::SkillsPaths;
use crate::skills::cli::interactive::is_interactive;
use crate::skills::cli::tui_picker::{TuiEntry, TuiEntryKind};
use anyhow::{Result, anyhow};
use clap::{Parser, Subcommand, ValueEnum};
use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Parser)]
#[command(about = "Manage agent presets")]
#[command(subcommand_required = true)]
pub struct AgentsArgs {
#[arg(long = "skills-dir", global = true)]
pub skills_dir: Option<PathBuf>,
#[command(subcommand)]
pub command: AgentsCommand,
}
#[derive(Subcommand)]
pub enum AgentsCommand {
New {
id: String,
#[arg(long)]
force: bool,
#[arg(long)]
ai: bool,
},
GenCode {
id: String,
#[arg(long, value_enum)]
framework: FrameworkArg,
#[arg(long)]
out: PathBuf,
#[arg(long)]
force: bool,
},
}
#[derive(ValueEnum, Debug, Clone, Copy)]
pub enum FrameworkArg {
#[value(name = "pydantic-ai")]
PydanticAi,
#[value(name = "crewai")]
CrewAi,
}
pub fn run(args: &AgentsArgs) -> Result<()> {
match &args.command {
AgentsCommand::New { id, force, ai } => {
run_new(id, *force, *ai, args.skills_dir.as_deref())
}
AgentsCommand::GenCode {
id,
framework,
out,
force,
} => crate::agents::codegen::run_gen_code(
id,
*framework,
out,
*force,
args.skills_dir.as_deref(),
),
}
}
fn run_new(id: &str, force: bool, ai: bool, skills_dir_override: Option<&Path>) -> Result<()> {
let agent_id =
validate_path_segment(id, "agent id").map_err(|e| anyhow!("invalid agent id: {e}"))?;
let interactive = is_interactive();
let config_dir = resolve_config_dir(None)?;
let paths = SkillsPaths::resolve_with_override(skills_dir_override)?;
paths.ensure_dirs()?;
let agent_skill_dir = paths.root.join(&agent_id);
let agent_skill_file = agent_skill_dir.join("SKILL.md");
let agent_manifest_dir = config_dir.join("agents").join(&agent_id);
let agent_manifest_file = agent_manifest_dir.join("agent.toml");
if !force && (agent_skill_dir.exists() || agent_manifest_dir.exists()) {
return Err(anyhow!(
"Agent preset already exists: {} or {} (use --force to overwrite)",
agent_skill_dir.display(),
agent_manifest_dir.display()
));
}
let discovered = discover_skills(&paths.root)?;
let mut ai_output: Option<AgentPresetBuildOutput> = None;
let mut includes = Vec::<String>::new();
if ai {
let output = run_ai_builder(&agent_id, &discovered)?;
includes = output.includes.clone();
ai_output = Some(output);
} else if interactive {
let picked = pick_includes_tui(&agent_id, &paths.root, &discovered)?;
let Some(picked) = picked else {
println!("{}", t!("messages.operation_cancelled"));
return Ok(());
};
includes = picked.into_iter().collect::<Vec<_>>();
}
includes.retain(|skill_id| skill_id != &agent_id);
includes.sort();
includes.dedup();
if interactive && !ai {
}
let mut manifest = AgentManifestV1::new(agent_id.clone(), includes);
if let Some(output) = &ai_output {
manifest.description = Some(output.description.clone());
}
manifest.normalize();
let skill_md = match &ai_output {
Some(output) => format!(
"---\nname: {agent_id}\n---\n\n{}\n",
output.system_prompt_md.trim_end()
),
None => default_agent_skill_markdown(&agent_id),
};
if force {
remove_path_if_exists(&agent_skill_dir)?;
remove_path_if_exists(&agent_manifest_dir)?;
}
fs::create_dir_all(&agent_skill_dir)?;
atomic_write_with_mode(&agent_skill_file, skill_md.as_bytes(), None)?;
fs::create_dir_all(&agent_manifest_dir)?;
manifest.write_to_path(&agent_manifest_file)?;
Ok(())
}
fn default_agent_skill_markdown(id: &str) -> String {
format!(
"---\nname: {id}\n---\n\nWrite your system prompt for `{id}` here.\n\n## Requirements\n\n- \n"
)
}
fn pick_includes_tui(
agent_id: &str,
skills_root: &Path,
discovered: &[crate::skills::catalog::types::SkillCandidate],
) -> Result<Option<HashSet<String>>> {
let mut entries = Vec::<TuiEntry>::new();
for skill in discovered {
let dir_name = skill
.skill_dir
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(&skill.skill_id);
let label = format!("{} ({})", skill.skill_id, dir_name);
entries.push(TuiEntry {
label,
kind: TuiEntryKind::Skill {
skill_id: skill.skill_id.clone(),
},
});
}
let defaults = HashSet::new();
crate::skills::cli::tui_picker::pick(
&format!(
"Select includes for {agent_id} (from {})",
skills_root.display()
),
&entries,
&defaults,
)
}
fn remove_path_if_exists(path: &Path) -> Result<()> {
if !path.exists() {
return Ok(());
}
let meta = fs::symlink_metadata(path)?;
if meta.is_dir() {
fs::remove_dir_all(path)?;
} else {
fs::remove_file(path)?;
}
Ok(())
}
fn run_ai_builder(
id: &str,
discovered: &[crate::skills::catalog::types::SkillCandidate],
) -> Result<AgentPresetBuildOutput> {
#[cfg(not(feature = "agents-ai"))]
{
let _ = (id, discovered);
Err(anyhow!(
"`--ai` requires building llman with feature `agents-ai` (rebuild with `cargo +nightly build --features agents-ai`)."
))
}
#[cfg(feature = "agents-ai")]
{
let available_skill_ids = discovered
.iter()
.map(|skill| skill.skill_id.clone())
.filter(|skill_id| skill_id != id)
.collect::<Vec<_>>();
let request = crate::agents::builder::AgentPresetBuildRequest {
agent_id: id.to_string(),
available_skill_ids,
};
crate::agents::builder::build_with_openai(&request)
}
}