llman 0.0.30

A tool for managing LLM application rules(prompts) ...
Documentation
use crate::agents::command::FrameworkArg;
use crate::agents::manifest::load_agent_manifest_v1;
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::types::SkillsPaths;
use crate::skills::cli::interactive::is_interactive;
use anyhow::{Context, Result, anyhow};
use inquire::Confirm;
use inquire::error::InquireError;
use minijinja::{Environment, context};
use std::fs;
use std::path::{Path, PathBuf};

pub fn run_gen_code(
    id: &str,
    framework: FrameworkArg,
    out: &PathBuf,
    force: 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)?;

    let agent_skill_file = paths.root.join(&agent_id).join("SKILL.md");
    if !agent_skill_file.exists() {
        return Err(anyhow!(
            "Missing agent-skill file: {} (run `llman agents new {}` first)",
            agent_skill_file.display(),
            agent_id
        ));
    }

    let manifest_file = config_dir.join("agents").join(&agent_id).join("agent.toml");
    if !manifest_file.exists() {
        return Err(anyhow!(
            "Missing agent manifest: {} (run `llman agents new {}` first)",
            manifest_file.display(),
            agent_id
        ));
    }

    fs::create_dir_all(out)?;
    let out_file = out.join("agent.py");
    if out_file.exists() && !force {
        if !interactive {
            return Err(anyhow!(
                "Output file already exists: {} (use --force to overwrite)",
                out_file.display()
            ));
        }
        let prompt = t!("messages.file_exists_overwrite", path = out_file.display());
        let confirm = match Confirm::new(&prompt).with_default(false).prompt() {
            Ok(confirm) => confirm,
            Err(InquireError::OperationCanceled | InquireError::OperationInterrupted) => {
                println!("{}", t!("messages.operation_cancelled"));
                return Ok(());
            }
            Err(e) => return Err(anyhow!("overwrite confirmation prompt failed: {}", e)),
        };
        if !confirm {
            println!("{}", t!("messages.operation_cancelled"));
            return Ok(());
        }
    }

    let manifest = load_agent_manifest_v1(&manifest_file)?;
    let skill_raw = fs::read_to_string(&agent_skill_file)
        .with_context(|| format!("read {}", agent_skill_file.display()))?;
    let system_prompt = extract_skill_body_markdown(&skill_raw).trim().to_string();
    let system_prompt_json = serde_json::to_string(&system_prompt).context("json encode prompt")?;

    let template = match framework {
        FrameworkArg::PydanticAi => TEMPLATE_PYDANTIC_AI,
        FrameworkArg::CrewAi => TEMPLATE_CREWAI,
    };

    let mut env = Environment::new();
    env.add_template("agent.py", template)
        .context("load agent.py template")?;
    let tmpl = env
        .get_template("agent.py")
        .context("get agent.py template")?;
    let rendered = tmpl
        .render(context! {
            agent_id => agent_id,
            system_prompt_json => system_prompt_json,
            includes => manifest.includes,
            skills_meta => manifest.skills,
        })
        .context("render agent.py template")?;

    atomic_write_with_mode(&out_file, rendered.as_bytes(), None)
        .with_context(|| format!("write {}", out_file.display()))?;
    Ok(())
}

const TEMPLATE_PYDANTIC_AI: &str = include_str!("../../templates/agents/pydantic-ai/agent.py.j2");
const TEMPLATE_CREWAI: &str = include_str!("../../templates/agents/crewai/agent.py.j2");

fn extract_skill_body_markdown(raw: &str) -> &str {
    let mut chunks = raw.split_inclusive('\n');
    let Some(first) = chunks.next() else {
        return raw;
    };
    if first.trim_end_matches(['\r', '\n']) != "---" {
        return raw;
    }

    let mut offset = first.len();
    for chunk in chunks {
        offset += chunk.len();
        if chunk.trim() == "---" {
            break;
        }
    }

    raw.get(offset..).unwrap_or("")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn extract_body_ignores_frontmatter() {
        let raw = "---\nname: test\n---\n\nhello\nworld\n";
        assert_eq!(extract_skill_body_markdown(raw), "\nhello\nworld\n");
    }
}