Skip to main content

oxios_kernel/skill/
prompt.rs

1#![allow(missing_docs)]
2//! Prompt formatting.
3
4use super::types::SkillEntry;
5use std::path::Path;
6
7pub fn escape_xml(s: &str) -> String {
8    s.replace('&', "&")
9        .replace('<', "&lt;")
10        .replace('>', "&gt;")
11        .replace('"', "&quot;")
12        .replace('\'', "&apos;")
13}
14
15pub fn compact_path(path: &Path) -> String {
16    if let Some(home) = dirs::home_dir() {
17        let home_str = home.to_string_lossy();
18        let path_str = path.to_string_lossy();
19        if let Some(rest) = path_str.strip_prefix(home_str.as_ref()) {
20            return format!("~{rest}");
21        }
22    }
23    path.to_string_lossy().into_owned()
24}
25
26pub fn format_skills_for_prompt(skills: &[&SkillEntry]) -> String {
27    if skills.is_empty() {
28        return String::new();
29    }
30    let mut lines = vec![
31        "\n\nThe following skills provide specialized instructions for specific tasks.".into(),
32        "Use the read tool to load a skill's file when the task matches its description.".into(),
33        "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.".into(),
34        String::new(),
35        "<available_skills>".into(),
36    ];
37    for skill in skills {
38        lines.push("  <skill>".into());
39        lines.push(format!(
40            "    <name>{}</name>",
41            escape_xml(&skill.skill.name)
42        ));
43        lines.push(format!(
44            "    <description>{}</description>",
45            escape_xml(&skill.skill.description)
46        ));
47        lines.push(format!(
48            "    <location>{}</location>",
49            escape_xml(&compact_path(&skill.skill.file_path))
50        ));
51        lines.push("  </skill>".into());
52    }
53    lines.push("</available_skills>".into());
54    lines.join("\n")
55}