Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fb65c257d | ||
|
|
068940495a |
Generated
+1
-1
@@ -21,7 +21,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-manager"
|
||||
version = "1.1.0"
|
||||
version = "1.1.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "agent-manager"
|
||||
version = "1.1.0"
|
||||
version = "1.1.2"
|
||||
edition = "2021"
|
||||
description = "Manage local AI coding agents: list, install, start, stop, update — with automatic dependency handling and a YAML-driven catalog."
|
||||
license = "MIT"
|
||||
|
||||
+20
@@ -62,6 +62,26 @@
|
||||
> 6 rôles copilot générés pour aichat (am-copilot, am-operator, am-dev,
|
||||
> am-do, am-analyst, am-orchestrator), `am ai --role <nom>`, tip
|
||||
> d'onboarding dans le banner REPL et les commandes IA (non-bloquant).
|
||||
>
|
||||
> 🔧 **Maintenance v1.1.1 (2026-08-20)** : fix `am setup` — le wizard écrivait
|
||||
> `providers.<nom>` au TOP-LEVEL de la config user (clé rejetée par le
|
||||
> validateur : « unknown field `providers` »). Le registre vivant sous
|
||||
> `settings.providers`, le wizard écrit désormais
|
||||
> `settings.providers.<nom>.base_url` + `.default_model` (le schéma exige
|
||||
> base_url quand un bloc provider est écrit). Test de régression
|
||||
> `setup_persists_under_settings_providers`.
|
||||
>
|
||||
> 🔧 **Maintenance v1.1.2 (2026-08-20)** :
|
||||
> - `am setup` : **openrouter** ajouté au choix des providers (base URL,
|
||||
> token via OPENROUTER_API_KEY, 6 modèles déclarés proposés) ;
|
||||
> le choix du modèle liste les modèles disponibles du provider.
|
||||
> - `am ai` sans `--role` utilise **am-copilot par défaut** (régénération
|
||||
> automatique des rôles si le fichier manque, dégradation douce).
|
||||
> - Bug `am migrate --import` : la config importée était écrite dans le
|
||||
> répertoire config GLOBAL (user_config_dir) au lieu du config actif de
|
||||
> l'app — l'import écrasait la config réelle de la machine (et la suite
|
||||
> de tests détruisait la config utilisateur à chaque `cargo test`).
|
||||
> L'import respecte désormais `app.paths.config_dir` (--config/isolated).
|
||||
|
||||
---
|
||||
|
||||
|
||||
+10
@@ -108,6 +108,16 @@ settings:
|
||||
base_url: https://generativelanguage.googleapis.com/v1beta
|
||||
default_model: gemini-2.5-pro
|
||||
models: [gemini-2.5-pro, gemini-2.5-flash]
|
||||
openrouter:
|
||||
base_url: https://openrouter.ai/api/v1
|
||||
default_model: deepseek/deepseek-chat-v3-0324
|
||||
models:
|
||||
- deepseek/deepseek-chat-v3-0324
|
||||
- anthropic/claude-sonnet-4.5
|
||||
- openai/gpt-4o
|
||||
- google/gemini-2.5-pro
|
||||
- meta-llama/llama-3.3-70b-instruct
|
||||
- mistralai/mistral-large-2411
|
||||
|
||||
# --- Command aliases ----------------------------------------------------------
|
||||
aliases:
|
||||
|
||||
+26
-11
@@ -75,21 +75,36 @@ fn aichat_installed(app: &App) -> bool {
|
||||
|| which::which("aichat").is_ok()
|
||||
}
|
||||
|
||||
/// Validate the --role flag: the file must exist among the generated
|
||||
/// am-* roles (v1.1.0 F3). Returns the aichat --agent arguments.
|
||||
/// Build the aichat `--agent` arguments for the requested role. Without a
|
||||
/// `--role` flag the am-copilot role is used by default (v1.1.2): when its
|
||||
/// file is missing the roles are regenerated on the fly, and a generation
|
||||
/// failure degrades gracefully (no role). An explicit unknown role is an
|
||||
/// error.
|
||||
fn role_args(app: &App, role: Option<&str>) -> Result<Vec<String>> {
|
||||
let Some(role) = role else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let role = role.unwrap_or("am-copilot");
|
||||
let dir = crate::roles::aichat_agents_dir();
|
||||
let path = dir.join(format!("{role}.md"));
|
||||
if !path.exists() {
|
||||
bail!(crate::i18n::tr_in(
|
||||
app.lang(),
|
||||
"rôle '{}' inconnu — générez les rôles avec: am setup --roles (am-copilot, am-operator, am-dev, am-do, am-analyst, am-orchestrator)"
|
||||
).replace("{}", role));
|
||||
if path.exists() {
|
||||
return Ok(vec!["--agent".to_string(), role.to_string()]);
|
||||
}
|
||||
Ok(vec!["--agent".to_string(), role.to_string()])
|
||||
if role == "am-copilot" {
|
||||
if let Err(e) = crate::roles::generate_all(app) {
|
||||
app.log.verbose(&format!("roles generation skipped: {e:#}"));
|
||||
}
|
||||
if path.exists() {
|
||||
return Ok(vec!["--agent".to_string(), role.to_string()]);
|
||||
}
|
||||
app.log.warn(crate::i18n::tr_in(
|
||||
app.lang(),
|
||||
"rôle 'am-copilot' introuvable — lancez am setup --roles",
|
||||
));
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
bail!(crate::i18n::tr_in(
|
||||
app.lang(),
|
||||
"rôle '{}' inconnu — générez les rôles avec: am setup --roles (am-copilot, am-operator, am-dev, am-do, am-analyst, am-orchestrator)"
|
||||
)
|
||||
.replace("{}", role));
|
||||
}
|
||||
|
||||
/// Conversational mode: aichat -f <ctx> "<prompt>" in the foreground, with
|
||||
|
||||
@@ -266,9 +266,14 @@ pub fn import_bundle(app: &App, bundle: &Path, confirm: bool) -> Result<Vec<Stri
|
||||
for (bf, data) in manifest.files.iter().zip(blobs.iter()) {
|
||||
let rel = Path::new(&bf.path);
|
||||
let target = if bf.path == "config.yaml" {
|
||||
// Config goes to the user config directory of machine B.
|
||||
let dir = crate::config::user_config_dir()
|
||||
.unwrap_or_else(|| crate::config::home_dir().unwrap_or_else(|| PathBuf::from(".")));
|
||||
// Config goes to the ACTIVE user config directory of this app
|
||||
// (respects --config / isolated tests) — NOT the machine-global
|
||||
// user config dir: importing must not clobber the real config.
|
||||
let dir = app
|
||||
.paths
|
||||
.config_dir
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("cannot determine the user config directory (no HOME set)"))?;
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
dir.join(crate::config::CONFIG_FILE_NAME)
|
||||
} else {
|
||||
@@ -292,8 +297,11 @@ pub fn import_bundle(app: &App, bundle: &Path, confirm: bool) -> Result<Vec<Stri
|
||||
// confirmed; otherwise it is written next to it as config.migrated.yaml.
|
||||
let cfg_rel = "config.yaml";
|
||||
if restored.iter().any(|r| r == cfg_rel) && !confirm {
|
||||
let dir = crate::config::user_config_dir()
|
||||
.unwrap_or_else(|| crate::config::home_dir().unwrap_or_else(|| PathBuf::from(".")));
|
||||
let dir = app
|
||||
.paths
|
||||
.config_dir
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("cannot determine the user config directory (no HOME set)"))?;
|
||||
let imported = dir.join(crate::config::CONFIG_FILE_NAME);
|
||||
if imported.exists() {
|
||||
let target = dir.join("config.migrated.yaml");
|
||||
|
||||
@@ -180,6 +180,7 @@ pub const CATALOG: &[(&str, &str)] = &[
|
||||
("Config aichat créée (provider + modèle)", "aichat config created (provider + model)"),
|
||||
("am est configuré — essayez: am ai \"...\" ou am ai --exec \"...\"", "am is configured — try: am ai \"...\" or am ai --exec \"...\""),
|
||||
("rôle '{}' inconnu — générez les rôles avec: am setup --roles (am-copilot, am-operator, am-dev, am-do, am-analyst, am-orchestrator)", "unknown role '{}' — generate the roles with: am setup --roles (am-copilot, am-operator, am-dev, am-do, am-analyst, am-orchestrator)"),
|
||||
("rôle 'am-copilot' introuvable — lancez am setup --roles", "role 'am-copilot' not found — run am setup --roles"),
|
||||
("usage: ask <demande> — ex: ask installe claude et lance-le", "usage: ask <request> — e.g. ask installe claude et lance-le"),
|
||||
("aucune source enregistrée — settings.registry.sources ou: am registry install <url>", "no sources registered — settings.registry.sources or: am registry install <url>"),
|
||||
("installation refusée — source non fiable", "installation refused — untrusted source"),
|
||||
|
||||
+58
-5
@@ -16,7 +16,7 @@ use std::io::Write;
|
||||
|
||||
/// Known providers offered by the wizard (from the embedded registry plus
|
||||
/// the local Ollama runtime and a custom endpoint).
|
||||
const CHOICES: &[&str] = &["anthropic", "openai", "deepseek", "google", "ollama", "custom"];
|
||||
const CHOICES: &[&str] = &["anthropic", "openai", "deepseek", "google", "openrouter", "ollama", "custom"];
|
||||
|
||||
/// True when the user has not configured a usable default provider yet:
|
||||
/// no `settings.default_provider`, or the default provider has no API
|
||||
@@ -229,10 +229,26 @@ pub fn run(app: &App) -> Result<i32> {
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Default model.
|
||||
// 3) Default model. The registry lives under settings.providers — a
|
||||
// top-level `providers:` key is rejected by the schema validator, and a
|
||||
// provider block written to the user config requires base_url too.
|
||||
let model = pick_model(app, &provider)?;
|
||||
if provider != "custom" {
|
||||
config_cmd::persist_setting(app, &format!("providers.{provider}.default_model"), &model)?;
|
||||
if provider == "ollama" {
|
||||
// base_url was already written by the Add above.
|
||||
config_cmd::persist_setting(app, "settings.providers.ollama.default_model", &model)?;
|
||||
} else if provider != "custom" {
|
||||
if let Some(def) = crate::providers::get(&app.config, &provider) {
|
||||
config_cmd::persist_setting(
|
||||
app,
|
||||
&format!("settings.providers.{provider}.base_url"),
|
||||
&def.base_url,
|
||||
)?;
|
||||
config_cmd::persist_setting(
|
||||
app,
|
||||
&format!("settings.providers.{provider}.default_model"),
|
||||
&model,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
// 4) Default provider.
|
||||
@@ -295,11 +311,16 @@ mod tests {
|
||||
use crate::app::App;
|
||||
use crate::cli::Cli;
|
||||
use clap::Parser;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
static COUNTER: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
fn app_with(settings_yaml: &str) -> App {
|
||||
app_with_path(settings_yaml).0
|
||||
}
|
||||
|
||||
fn app_with_path(settings_yaml: &str) -> (App, PathBuf) {
|
||||
let id = COUNTER.fetch_add(1, Ordering::SeqCst);
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"am-setup-test-{}-{id}",
|
||||
@@ -315,7 +336,7 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
let cli = Cli::parse_from(["am", "--config", cfg.to_str().unwrap()]);
|
||||
App::from_cli(cli).unwrap()
|
||||
(App::from_cli(cli).unwrap(), cfg)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -333,4 +354,36 @@ mod tests {
|
||||
);
|
||||
assert!(!needs_setup(&app));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn setup_persists_under_settings_providers() {
|
||||
// Regression: a top-level `providers:` key is rejected by the
|
||||
// schema validator ("unknown field `providers`"). The wizard must
|
||||
// write settings.providers.<name>.default_model.
|
||||
let (app, cfg) = app_with_path("");
|
||||
config_cmd::persist_setting(
|
||||
&app,
|
||||
"settings.providers.deepseek.base_url",
|
||||
"https://api.deepseek.com",
|
||||
)
|
||||
.unwrap();
|
||||
config_cmd::persist_setting(
|
||||
&app,
|
||||
"settings.providers.deepseek.default_model",
|
||||
"deepseek-reasoner",
|
||||
)
|
||||
.unwrap();
|
||||
config_cmd::persist_setting(&app, "settings.default_provider", "deepseek").unwrap();
|
||||
|
||||
// Reloading the config must succeed (validation passes) and the
|
||||
// merged registry must reflect the new default model.
|
||||
let cli = Cli::parse_from(["am", "--config", cfg.to_str().unwrap()]);
|
||||
let app2 = App::from_cli(cli).expect("config must stay valid after setup writes");
|
||||
let def = crate::providers::get(&app2.config, "deepseek").unwrap();
|
||||
assert_eq!(def.default_model.as_deref(), Some("deepseek-reasoner"));
|
||||
assert_eq!(
|
||||
crate::providers::default_name(&app2.config).as_deref(),
|
||||
Some("deepseek")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user