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:
+78
-8
@@ -105,6 +105,18 @@ pub struct Settings {
|
||||
/// Update policy: how aggressively 'am update' should act (issue #65).
|
||||
#[serde(default)]
|
||||
pub update_policy: Option<UpdatePolicy>,
|
||||
/// A model is a prune candidate after this many days without use
|
||||
/// (issue #73, default 30).
|
||||
#[serde(default)]
|
||||
pub models_prune_days: Option<u64>,
|
||||
/// Base URL of the official catalog used by 'am catalog update'
|
||||
/// (issue #60; default: the embedded catalog on Gitea).
|
||||
#[serde(default)]
|
||||
pub catalog_url: Option<String>,
|
||||
/// How long (seconds) a remote catalog stays cached before a refresh
|
||||
/// is attempted (issue #60, default 3600).
|
||||
#[serde(default)]
|
||||
pub catalog_cache_ttl_secs: Option<u64>,
|
||||
}
|
||||
|
||||
/// Policy controlling how aggressively 'am update' upgrades installed agents.
|
||||
@@ -204,6 +216,13 @@ pub struct AgentDef {
|
||||
/// Pinned version: 'am update' will refuse to install a different one (issue #65).
|
||||
#[serde(default)]
|
||||
pub pin_version: Option<String>,
|
||||
/// Environment variable carrying the model name at launch
|
||||
/// (issue #71, e.g. ANTHROPIC_MODEL for claude-code).
|
||||
#[serde(default)]
|
||||
pub model_env: Option<String>,
|
||||
/// CLI flag carrying the model name at launch (issue #71, e.g. "--model").
|
||||
#[serde(default)]
|
||||
pub model_arg: Option<String>,
|
||||
#[serde(default)]
|
||||
pub tags: Vec<String>,
|
||||
/// When false, the agent is listed but cannot be installed locally.
|
||||
@@ -513,7 +532,10 @@ pub fn parse_file(path: &Path) -> Result<Config> {
|
||||
fn load_file_with_includes(path: &Path, visited: &mut HashSet<PathBuf>) -> Result<Config> {
|
||||
let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
|
||||
if !visited.insert(canon.clone()) {
|
||||
return Err(anyhow!("config include cycle detected at {}", path.display()));
|
||||
return Err(anyhow!(
|
||||
"config include cycle detected at {}",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
let mut cfg = parse_file(path)?;
|
||||
let includes = cfg.include.clone();
|
||||
@@ -523,19 +545,55 @@ fn load_file_with_includes(path: &Path, visited: &mut HashSet<PathBuf>) -> Resul
|
||||
let mut merged = Config::default();
|
||||
merged.version = cfg.version.clone();
|
||||
for inc in &includes {
|
||||
let inc_path = expand_path(inc);
|
||||
let abs = if inc_path.is_absolute() {
|
||||
inc_path
|
||||
} else {
|
||||
base_dir.join(inc_path)
|
||||
};
|
||||
let sub = load_file_with_includes(&abs, visited)?;
|
||||
let sub = load_include(inc, base_dir, visited)?;
|
||||
merge(&mut merged, sub);
|
||||
}
|
||||
merge(&mut merged, cfg);
|
||||
Ok(merged)
|
||||
}
|
||||
|
||||
/// Load one include entry: a local path (relative to the including file) or
|
||||
/// a remote catalog URL (http/https/file, cached on disk, issue #67).
|
||||
fn load_include(inc: &str, base_dir: &Path, visited: &mut HashSet<PathBuf>) -> Result<Config> {
|
||||
let lower = inc.to_lowercase();
|
||||
if lower.starts_with("http://") || lower.starts_with("https://") {
|
||||
let state_dir = crate::app::state_dir();
|
||||
let text = crate::catalog_remote::fetch_for_config(&state_dir, inc)?;
|
||||
let sub = crate::catalog_remote::parse_catalog(&text)
|
||||
.with_context(|| format!("remote include {inc} is not a valid catalog"))?;
|
||||
// Remote includes can themselves reference other URLs; local paths
|
||||
// inside them are resolved against the config directory.
|
||||
let mut merged = Config::default();
|
||||
merged.version = sub.version.clone();
|
||||
for sub_inc in &sub.include {
|
||||
let nested = load_include(sub_inc, base_dir, visited)?;
|
||||
merge(&mut merged, nested);
|
||||
}
|
||||
let mut own = sub;
|
||||
own.include.clear();
|
||||
merge(&mut merged, own);
|
||||
return Ok(merged);
|
||||
}
|
||||
if lower.starts_with("file://") {
|
||||
let local = inc.strip_prefix("file://").unwrap_or(inc);
|
||||
// Windows: "file:///C:/x" -> "C:/x" (drop the leading slash).
|
||||
let local = if cfg!(windows) && local.starts_with('/') {
|
||||
local.strip_prefix('/').unwrap_or(local)
|
||||
} else {
|
||||
local
|
||||
};
|
||||
let abs = PathBuf::from(local);
|
||||
return load_file_with_includes(&abs, visited);
|
||||
}
|
||||
let inc_path = expand_path(inc);
|
||||
let abs = if inc_path.is_absolute() {
|
||||
inc_path
|
||||
} else {
|
||||
base_dir.join(inc_path)
|
||||
};
|
||||
load_file_with_includes(&abs, visited)
|
||||
}
|
||||
|
||||
/// Merge an overlay config into a base config: user definitions replace
|
||||
/// default agents with the same name; maps are merged key by key.
|
||||
pub fn merge(base: &mut Config, overlay: Config) {
|
||||
@@ -568,6 +626,18 @@ pub fn merge(base: &mut Config, overlay: Config) {
|
||||
if o.theme.is_some() {
|
||||
s.theme = o.theme;
|
||||
}
|
||||
if o.update_policy.is_some() {
|
||||
s.update_policy = o.update_policy;
|
||||
}
|
||||
if o.models_prune_days.is_some() {
|
||||
s.models_prune_days = o.models_prune_days;
|
||||
}
|
||||
if o.catalog_url.is_some() {
|
||||
s.catalog_url = o.catalog_url;
|
||||
}
|
||||
if o.catalog_cache_ttl_secs.is_some() {
|
||||
s.catalog_cache_ttl_secs = o.catalog_cache_ttl_secs;
|
||||
}
|
||||
for (k, v) in o.hooks {
|
||||
s.hooks.insert(k, v);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user