feat: am ai — langage naturel vers action shell securisee via AIChat (issues #96 #97)
release / release (push) Successful in 13m12s
release / macos (push) Canceled after 0s

- Commande am ai (alias CLI: am shell) : conversationnel par defaut
  (aichat -f <ctx> "<prompt>"), mode --exec avec generation aichat --dry-run
- Securite (issue #97) : classification safe/risky + certainite affichee,
  politique dry-run par defaut (settings.shell_ai.default_safety:
  dry-run | confirm | auto), confirmation y/N pour les commandes risky,
  --yes pour executer, --dry-run force la simulation
- Flags : --exec/-e, --files/-f (defaut: dossier courant), --provider,
  --model (registre providers -> env aichat + modele, token via keyring)
- Execution via le shell utilisateur (default_shell > SHELL > COMSPEC)
- Journalisation : EventKind::ShellAi (mode, risk, certainty, executed)
- Module src/shell_ai.rs (classification, politique, generation, execution)
- Câblage complet : cli, dispatch, help, REPL (parseur+completer+banner),
  tip, man pages (am-ai.1), i18n EN, config.yaml (bloc shell_ai)
- Tests : 7 unitaires (classification, certainty, extraction, decision)
  + 4 integration (shim aichat.cmd : dry-run par defaut, --yes, mode
  conversationnel, mode confirm) — 339 tests verts
- v1.0.5 ; ROADMAP axe 13.3 coche (6 lignes), epic #93 clôturee

closes #93
closes #96
closes #97
This commit is contained in:
2026-08-20 12:07:38 -04:00
parent 02517735be
commit 313645eea2
19 changed files with 1032 additions and 15 deletions
+56 -2
View File
@@ -114,6 +114,7 @@ const COMMAND_DESCRIPTIONS: &[(&str, &str)] = &[
("providers", "manage the LLM provider registry (base URLs, models, default)"),
("suggest", "recommend an agent for a request"),
("ask", "natural language → am commands (local rules first, optional LLM refinement)"),
("ai", "natural language → shell action via AIChat (--exec to run, dry-run by default)"),
("audit", "who changed what, when (config checksums)"),
("service", "register an agent as a system service (autostart)"),
("schedule", "plan am commands (daily) and check the fleet health"),
@@ -239,7 +240,7 @@ impl AmCompleter {
"start", "stop", "restart", "run", "doctor", "config", "completion",
"self-update", "self-uninstall", "export", "import", "shell", "theme", "lang",
"tip", "dashboard", "favorite", "unfavorite", "note", "tag", "untag", "tags",
"profile", "man", "models", "catalog", "providers", "suggest", "ask", "registry", "audit",
"profile", "man", "models", "catalog", "providers", "suggest", "ask", "ai", "registry", "audit",
"service", "schedule", "monitor", "web", "serve", "sync", "migrate", "playbook", "lab", "plugins",
"ls", "dir", "cd", "ps", "where", "get", "help", "version", "exit",
],
@@ -799,7 +800,7 @@ pub fn banner_box(
.to_string(),
));
rows.push(inner(
" models models · models --prune · catalog · suggest · ask · audit".to_string(),
" models models · models --prune · catalog · suggest · ask · ai · audit".to_string(),
));
rows.push(inner(
" automate service install · schedule add · doctor --watch · monitor · sync · migrate · playbook".to_string(),
@@ -1603,6 +1604,58 @@ fn handle_line(
yes: flag("--yes"),
}
},
"ai" => {
let mut exec = false;
let mut yes = false;
let mut dry_run = false;
let mut files: Vec<std::path::PathBuf> = Vec::new();
let mut provider: Option<String> = None;
let mut model: Option<String> = None;
let mut prompt: Vec<String> = Vec::new();
let mut i = 0;
while i < rest.len() {
match rest[i].as_str() {
"--exec" | "-e" => exec = true,
"--yes" | "-y" => yes = true,
"--dry-run" => dry_run = true,
"--files" | "-f" => {
i += 1;
if i < rest.len() {
files.push(std::path::PathBuf::from(&rest[i]));
}
}
"--provider" => {
i += 1;
if i < rest.len() {
provider = Some(rest[i].clone());
}
}
"--model" => {
i += 1;
if i < rest.len() {
model = Some(rest[i].clone());
}
}
other => prompt.push(other.to_string()),
}
i += 1;
}
if prompt.is_empty() {
app.log.error(crate::i18n::tr(
"usage: ai <demande> — ex: ai --exec \"compresse les fichiers JSON\"",
));
return Ok(false);
}
Command::Ai(crate::cli::AiArgs {
prompt,
exec,
files,
provider,
model,
yes,
dry_run,
})
},
"alias" => match rest.first().map(|s| s.as_str()) {
Some("add") if rest.len() >= 3 => Command::Alias(crate::cli::AliasCmd::Add {
name: rest[1].clone(),
@@ -2019,6 +2072,7 @@ pub fn is_am_command(word: &str) -> bool {
| "providers"
| "suggest"
| "ask"
| "ai"
| "audit"
| "service"
| "schedule"