feat: phase 2 — modèles locaux + catalogue distant + suggest (closes #70 #71 #73 #60 #67 #61)

Axe 11 (modèles) :
- am models : inventaire des runtimes locaux (ollama list --format json,
  llama.cpp .gguf sur disque, LM Studio API locale) avec cache façon
  probe.rs (TTL 30s) et absence de runtime signalée sans erreur fatale
- am run <agent> --model <m> : résolution dans l'inventaire, transmission
  via model_env/model_arg de l'agent (défaut AM_MODEL), tracé en session
- am models --prune : candidats inutilisés depuis settings.models_prune_days
  (défaut 30), :latest toujours protégé, dry-run + confirmation + événement
  EventKind::Model journalisé

Axes 7/9 (catalogue) :
- am catalog update : fetch du catalogue officiel, diff (ajoutés/retirés/
  modifiés), confirmation, enregistrement en include
- am catalog add <url> : validation du format (parse + validate), cache
  catalog-cache.json (TTL 3600s), événement EventKind::Catalog journalisé
- include par URL (http/https/file) dans config.yaml, résolu au chargement
  avec cache + TTL ; override local toujours prioritaire (issue #67)
- am suggest <requête> : réutilise le scoring fuzzy du catalogue, boost par
  usage réel (lancements + durée depuis le journal), top 5 justifié, --json

REPL : commandes models/catalog/suggest (parse, complétion, descriptions),
help specs, tip cheat sheet, man pages régénérées (49 pages, dont 3 nouvelles).
Version bump 0.4.5, ROADMAP cases cochées, 266 tests verts.
This commit is contained in:
2026-08-18 09:06:16 -04:00
parent 890ad953e5
commit 22bc529818
31 changed files with 2122 additions and 40 deletions
+39 -9
View File
@@ -217,7 +217,42 @@ fn add_file(app: &App, file: &Path) -> Result<i32> {
let dest = dir.join(&base);
std::fs::copy(file, &dest)
.with_context(|| format!("cannot copy {} to {}", file.display(), dest.display()))?;
register_include(app, &base)?;
app.log.success(&format!(
"added {} and registered it in {}",
dest.display(),
user_config_path(app)?.display()
));
Ok(0)
}
/// Read the `include:` entries of the user config file (they are consumed
/// at load time, so the resolved config no longer lists them).
pub fn user_includes(app: &App) -> Vec<String> {
let Some(cfg_path) = user_config_path_if_any(app) else {
return Vec::new();
};
let Ok(text) = std::fs::read_to_string(&cfg_path) else {
return Vec::new();
};
let Ok(value) = serde_yaml::from_str::<serde_yaml::Value>(&text) else {
return Vec::new();
};
value
.get("include")
.and_then(|i| i.as_sequence())
.map(|seq| {
seq.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default()
}
/// Add one entry to the `include:` list of the user config file (creating
/// the file with a template when it does not exist). Used by 'config add'
/// (local files) and 'catalog add/update' (remote URLs, issues #60 #67).
pub fn register_include(app: &App, entry: &str) -> Result<()> {
let cfg_path = user_config_path(app)?;
let text = std::fs::read_to_string(&cfg_path)?;
let mut value: serde_yaml::Value = serde_yaml::from_str(&text)
@@ -227,7 +262,7 @@ fn add_file(app: &App, file: &Path) -> Result<i32> {
let key = serde_yaml::Value::String("include".to_string());
match map.get_mut(&key) {
Some(serde_yaml::Value::Sequence(seq)) => {
let entry = serde_yaml::Value::String(base.clone());
let entry = serde_yaml::Value::String(entry.to_string());
if !seq.contains(&entry) {
seq.push(entry);
}
@@ -236,7 +271,7 @@ fn add_file(app: &App, file: &Path) -> Result<i32> {
map.insert(
key,
serde_yaml::Value::Sequence(vec![serde_yaml::Value::String(
base.clone(),
entry.to_string(),
)]),
);
}
@@ -247,16 +282,11 @@ fn add_file(app: &App, file: &Path) -> Result<i32> {
_ => {
std::fs::write(
&cfg_path,
format!("version: \"1.0\"\ninclude:\n - {base}\n"),
format!("version: \"1.0\"\ninclude:\n - {entry}\n"),
)?;
}
}
app.log.success(&format!(
"added {} and registered it in {}",
dest.display(),
cfg_path.display()
));
Ok(0)
Ok(())
}
#[cfg(test)]