#[cfg(feature = "cli")]
use {
anyhow::Result,
chrono::Utc,
serde::{Deserialize, Serialize},
std::collections::HashMap,
std::path::PathBuf,
};
#[cfg(feature = "cli")]
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Skill {
pub domain: String,
pub lessons: Vec<String>,
pub anti_patterns: Vec<String>,
pub updated_at: String,
}
#[cfg(feature = "cli")]
pub struct SkillStore {
pub skills: HashMap<String, Skill>,
pub store_dir: PathBuf,
}
#[cfg(feature = "cli")]
impl SkillStore {
pub fn new(store_dir: PathBuf) -> Self {
Self {
skills: HashMap::new(),
store_dir,
}
}
pub fn load(store_dir: PathBuf) -> Result<Self> {
std::fs::create_dir_all(&store_dir)?;
let mut skills = HashMap::new();
for entry in std::fs::read_dir(&store_dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("toml") {
continue;
}
let raw = std::fs::read_to_string(&path)?;
if let Ok(skill) = toml_edit::de::from_str::<Skill>(&raw) {
skills.insert(skill.domain.clone(), skill);
}
}
Ok(Self { skills, store_dir })
}
pub fn load_for_domain(prompt: &str, store_dir: PathBuf) -> Result<Self> {
let mut store = Self::load(store_dir)?;
let domains = detect_domains(prompt);
store.skills.retain(|domain, _| {
domains
.iter()
.any(|d| domain.contains(d.as_str()) || d.contains(domain.as_str()))
});
Ok(store)
}
pub fn save_lesson(
&mut self,
domain: &str,
lesson: &str,
anti_pattern: Option<&str>,
) -> Result<()> {
let skill = self
.skills
.entry(domain.to_string())
.or_insert_with(|| Skill {
domain: domain.to_string(),
lessons: Vec::new(),
anti_patterns: Vec::new(),
updated_at: Utc::now().to_rfc3339(),
});
let lesson = lesson.trim().to_string();
if !lesson.is_empty() && !skill.lessons.contains(&lesson) {
skill.lessons.push(lesson);
}
if let Some(ap) = anti_pattern {
let ap = ap.trim().to_string();
if !ap.is_empty() && !skill.anti_patterns.contains(&ap) {
skill.anti_patterns.push(ap);
}
}
skill.updated_at = Utc::now().to_rfc3339();
let path = self.store_dir.join(format!("{domain}.toml"));
let raw = toml_edit::ser::to_string_pretty(skill)?;
std::fs::write(path, raw)?;
Ok(())
}
pub fn to_prompt_context(&self) -> String {
if self.skills.is_empty() {
return String::new();
}
let mut lines = vec!["## Relevant Skills From Previous Sessions".to_string()];
let mut token_budget = 2400_usize;
for skill in self.skills.values() {
if token_budget == 0 {
break;
}
lines.push(format!("\n### {}", skill.domain));
token_budget = token_budget.saturating_sub(20);
for lesson in skill.lessons.iter().take(5) {
let entry = format!("- ✓ {lesson}");
token_budget = token_budget.saturating_sub(entry.len() + 1);
if token_budget == 0 {
break;
}
lines.push(entry);
}
for ap in skill.anti_patterns.iter().take(3) {
let entry = format!("- ✗ Avoid: {ap}");
token_budget = token_budget.saturating_sub(entry.len() + 1);
if token_budget == 0 {
break;
}
lines.push(entry);
}
}
lines.join("\n")
}
pub fn is_empty(&self) -> bool {
self.skills.is_empty()
}
}
#[cfg(feature = "cli")]
pub fn detect_domains(prompt: &str) -> Vec<String> {
let lower = prompt.to_lowercase();
let vocab: &[&str] = &[
"fastapi",
"django",
"flask",
"express",
"nestjs",
"axum",
"actix",
"react",
"vue",
"svelte",
"nextjs",
"nuxt",
"angular",
"rust",
"python",
"javascript",
"typescript",
"go",
"java",
"csharp",
"docker",
"kubernetes",
"terraform",
"ansible",
"postgres",
"mysql",
"sqlite",
"mongodb",
"redis",
"graphql",
"grpc",
"websocket",
"rest",
"jwt",
"oauth",
"auth",
"celery",
"kafka",
"rabbitmq",
"pytest",
"jest",
"cargo",
"npm",
"pip",
"uv",
];
vocab
.iter()
.filter(|&&kw| lower.contains(kw))
.map(|&kw| kw.to_string())
.collect()
}