diff --git a/Cargo.toml b/Cargo.toml index 53689dd..dd3f8a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "agent-manager" -version = "1.1.3" +version = "1.1.4" edition = "2021" description = "Manage local AI coding agents: list, install, start, stop, update — with automatic dependency handling and a YAML-driven catalog." license = "MIT" diff --git a/ROADMAP.md b/ROADMAP.md index e612f97..09cf583 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -92,6 +92,14 @@ > enregistré existant » (les branches non-installé/binaire manquant > étaient déjà correctes). Test de régression > `resolve_exec_installed_agent_does_not_leak_run_token`. +> +> 🔧 **Maintenance v1.1.4 (2026-08-21)** : hints de configuration +> post-install — nouveau champ `setup_hints` dans la définition d'un agent +> (config.yaml) : commandes de configuration manuelle affichées après +> l'install quand l'agent gère sa propre config (ex. picoclaw +> onboard/model/auth), avec substitution des placeholders `{model}`, +> `{provider}` et `{base_url}` depuis le provider résolu. picoclaw est le +> premier agent du catalogue à les déclarer. --- diff --git a/config.yaml b/config.yaml index 81e1650..8c28a42 100644 --- a/config.yaml +++ b/config.yaml @@ -1025,6 +1025,12 @@ agents: repo: sipeed/picoclaw binary: picoclaw run: picoclaw + # PicoClaw gère sa config par sous-commandes (pas de fichier à écrire) : + # ces commandes sont affichées après l'install avec {model} substitué. + setup_hints: + - "picoclaw onboard" + - "picoclaw model {model}" + - "picoclaw auth login" tags: [go, tiny] - name: ironclaw diff --git a/src/agent_config.rs b/src/agent_config.rs index 344de48..d29b3d4 100644 --- a/src/agent_config.rs +++ b/src/agent_config.rs @@ -111,6 +111,46 @@ pub fn apply_env_map_resolved( } } +/// Substitute the {model}, {provider} and {base_url} placeholders of a +/// setup hint with the resolved provider configuration (v1.1.4). When no +/// provider is resolved, the hint is returned unchanged. +pub fn render_hint( + hint: &str, + resolved: &Option<(String, Option)>, + config: &Config, +) -> String { + let Some((pname, model)) = resolved else { + return hint.to_string(); + }; + let base_url = crate::providers::get(config, pname) + .map(|d| d.base_url.clone()) + .unwrap_or_default(); + hint.replace("{provider}", pname) + .replace( + "{model}", + model.as_deref().unwrap_or("{model}"), + ) + .replace("{base_url}", &base_url) +} + +/// Print the manual setup commands of an agent (agents that manage their +/// own configuration, e.g. picoclaw onboard/model/auth). No-op when the +/// agent declares none (v1.1.4). +fn print_hints( + app: &App, + agent: &AgentDef, + resolved: &Option<(String, Option)>, +) { + if agent.setup_hints.is_empty() { + return; + } + app.log + .success(crate::i18n::tr("Configuration manuelle requise — à exécuter :")); + for hint in &agent.setup_hints { + app.log.info(&format!(" {}", render_hint(hint, resolved, &app.config))); + } +} + /// Write the agent configuration files after a successful install /// (issue #91). The caller skips this on `--no-config`, on non-configurable /// agents, and in dry-run (never reached). Never writes a token in clear: @@ -124,6 +164,7 @@ pub fn apply_post_install( app.log.info(crate::i18n::tr( "agent sans adaptateur de config — voir sa doc", )); + print_hints(app, agent, resolved); return Ok(()); }; if cfg.files.is_empty() { @@ -136,12 +177,14 @@ pub fn apply_post_install( "config par variables d'env — injectée au démarrage", )); } + print_hints(app, agent, resolved); return Ok(()); } let Some((pname, model)) = resolved else { app.log.warn(crate::i18n::tr( "aucun provider résolu — fichiers de config non écrits", )); + print_hints(app, agent, resolved); return Ok(()); }; let Some(def) = crate::providers::get(&app.config, pname) else { @@ -418,6 +461,7 @@ mod tests { provider, model: None, config: Some(config), + setup_hints: vec![], sandbox: None, tags: vec![], installable: false, @@ -515,4 +559,64 @@ mod tests { ); assert_eq!(provider_pref(&agent2), Some("openai")); } + + #[test] + fn render_hint_substitutes_resolved_values() { + let c: Config = serde_yaml::from_str( + "version: \"1.0\"\nsettings:\n default_provider: openrouter\n providers:\n openrouter:\n base_url: https://openrouter.ai/api/v1\n default_model: deepseek/deepseek-chat-v3-0324\n models: [deepseek/deepseek-chat-v3-0324]\n", + ) + .unwrap(); + let resolved = Some(("openrouter".to_string(), Some("deepseek/deepseek-chat-v3-0324".to_string()))); + assert_eq!( + render_hint("picoclaw model {model}", &resolved, &c), + "picoclaw model deepseek/deepseek-chat-v3-0324" + ); + assert_eq!(render_hint("picoclaw onboard", &resolved, &c), "picoclaw onboard"); + assert_eq!( + render_hint("cli --provider {provider} --url {base_url}", &resolved, &c), + "cli --provider openrouter --url https://openrouter.ai/api/v1" + ); + // No resolved provider: the hint is left untouched. + assert_eq!( + render_hint("picoclaw model {model}", &None, &c), + "picoclaw model {model}" + ); + // Resolved provider without a model: {model} stays visible. + let no_model = Some(("openrouter".to_string(), None)); + assert_eq!( + render_hint("picoclaw model {model}", &no_model, &c), + "picoclaw model {model}" + ); + } + + #[test] + fn apply_post_install_with_setup_hints_is_ok() { + let guard = tempfile::tempdir().unwrap(); + let dir = guard.path().to_path_buf(); + std::mem::forget(guard); + let cfg = dir.join("config.yaml"); + std::fs::write( + &cfg, + "version: \"1.0\"\nsettings:\n auto_install_deps: false\n confirm_before_run: false\nagents: []\n", + ) + .unwrap(); + let cli = crate::cli::Cli::parse_from(["am", "--config", cfg.to_str().unwrap()]); + let app = crate::app::App::from_cli(cli).unwrap(); + let mut agent = agent_with_config( + AgentConfig { + env_map: BTreeMap::new(), + files: vec![], + provider_default: None, + }, + None, + ); + agent.config = None; + agent.setup_hints = vec![ + "picoclaw onboard".to_string(), + "picoclaw model {model}".to_string(), + ]; + // No provider resolved: hints printed with placeholders, no crash. + assert!(apply_post_install(&app, &agent, &None).is_ok()); + assert!(apply_post_install(&app, &agent, &Some(("openrouter".to_string(), None))).is_ok()); + } } diff --git a/src/commands/run_cmd.rs b/src/commands/run_cmd.rs index e84fff4..37b1779 100644 --- a/src/commands/run_cmd.rs +++ b/src/commands/run_cmd.rs @@ -669,6 +669,7 @@ mod tests { files: vec![], provider_default: None, }), + setup_hints: vec![], sandbox: None, tags: vec![], installable: false, diff --git a/src/commands/suggest_cmd.rs b/src/commands/suggest_cmd.rs index a97f7e1..f9f8640 100644 --- a/src/commands/suggest_cmd.rs +++ b/src/commands/suggest_cmd.rs @@ -200,6 +200,7 @@ mod tests { provider: None, model: None, config: None, + setup_hints: vec![], sandbox: None, tags: tags.iter().map(|s| s.to_string()).collect(), installable: false, diff --git a/src/config.rs b/src/config.rs index 816ad85..3d44f33 100644 --- a/src/config.rs +++ b/src/config.rs @@ -467,6 +467,12 @@ pub struct AgentDef { /// Post-install provider configuration (issue #91). #[serde(default)] pub config: Option, + /// Manual setup commands shown after install when the agent manages its + /// own configuration (onboard/model/auth...). Placeholders {model}, + /// {provider} and {base_url} are substituted with the resolved values + /// (v1.1.4). + #[serde(default)] + pub setup_hints: Vec, /// Sandbox profile (issue #79): allowed commands, working directories /// and network policy. Disabled by default — the unsandboxed mode /// stays available and documented. diff --git a/src/i18n.rs b/src/i18n.rs index 0b52633..e19be1b 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -214,6 +214,7 @@ pub const CATALOG: &[(&str, &str)] = &[ ("provider {} (modèle {}) au lancement de {}", "provider {} (model {}) when launching {}"), ("agent sans adaptateur de config — voir sa doc", "agent without a config adapter — see its docs"), ("config par variables d'env — injectée au démarrage", "env-var configuration — injected at start"), + ("Configuration manuelle requise — à exécuter :", "Manual setup required — run:"), ("aucun provider résolu — fichiers de config non écrits", "no provider resolved — config files not written"), ("provider '{}' introuvable — fichiers de config non écrits", "provider '{}' not found — config files not written"), ("format inconnu pour {} — adaptateurs: toml, yaml, json, key=value", "unknown format for {} — adapters: toml, yaml, json, key=value"),