Rust middleware for CLI tools that makes them speak natively to AI agents β with adapters for clap and argh.
Full documentation: murli.allankent.com
[dependencies]
murli = { version = "0.1", features = ["clap"] } # clap adapter
murli = { version = "0.1", features = ["argh"] } # argh adapter
murli = { version = "0.1", features = ["clap", "argh"] } # bothuse clap::{CommandFactory, Parser};
use murli::clap::GlobalArgs;
use murli::Writer;
use serde_json::json;
#[derive(Parser)]
#[command(name = "mytool", about = "My deployment tool")]
struct Cli {
#[command(flatten)]
murli: GlobalArgs, // adds --agent --schema --force --dry-run --output --profile
#[command(subcommand)]
command: Commands,
}
#[derive(clap::Subcommand)]
enum Commands {
Deploy { env: String },
}
fn main() {
let args = Cli::parse();
// handles --schema and mutation guard; exits if consumed
murli::clap::handle_builtins(&args.murli, &Cli::command(), None);
let mut writer = Writer::from_args(&args.murli);
match args.command {
Commands::Deploy { env } => {
if writer.is_dry_run() {
writer.write_plan(&format!("Would deploy to {env}"),
&json!({"env": env}));
} else {
writer.write_success(&format!("Deployed to {env}"),
&json!({"env": env}));
}
}
}
}For builder-API users, enable() adds all murli flags and mounts describe, doctor, profile as real subcommands:
fn main() {
let mut cmd = build_my_command();
murli::clap::enable(&mut cmd);
let matches = cmd.get_matches();
// exits if describe/doctor/profile/--schema was invoked
murli::clap::handle_matches(&matches, &build_my_command());
let writer = murli::Writer::new(
matches.get_flag("agent"),
None, false, false, env!("CARGO_PKG_VERSION"),
);
// dispatch your commands using matches
}Note: argh 0.1.x does not support
#[argh(flatten)]. ParseGlobalArgsseparately alongside your own args struct, or wait for flatten support in a future argh release.
use argh::FromArgs;
use murli::argh::GlobalArgs;
use serde_json::json;
#[derive(FromArgs, Debug)]
#[argh(description = "My deployment tool")]
struct Cli {
/// Target environment
#[argh(option)]
env: String,
}
fn main() {
// Parse murli flags separately
let murli_args = GlobalArgs::from_args(&["mytool"], &std::env::args().skip(1).collect::<Vec<_>>()
.iter().map(|s| s.as_str()).collect::<Vec<_>>())
.unwrap_or_default();
murli::argh::handle_builtins(&murli_args, None, "mytool", "My deployment tool");
let mut writer = murli::argh::writer_from_args(&murli_args);
let args: Cli = argh::from_env();
writer.write_success(&format!("Deployed to {}", args.env), &json!({"env": args.env}));
}use murli::{AgentError, EXIT_NOT_FOUND};
// Convenience constructors
writer.write_error(AgentError::user_error("flag --env is required", "pass --env prod"));
writer.write_error(AgentError::not_found("index not found", "run `mytool index build`"));
writer.write_error(AgentError::rate_limited("API limit hit", 5000));
// Full control
writer.write_error(AgentError {
code: EXIT_NOT_FOUND,
error_type: "index_missing".into(),
message: "Semantic index not found".into(),
suggestion: "Run `mytool index build`".into(),
recoverable: false,
doc_url: "https://example.com/docs".into(),
..Default::default()
});Error envelopes are written to stderr. The process exits with the error's code. For tests, use write_error_to_streams (writes without exiting).
Success (stdout):
{
"status": "ok",
"schema_version": "1.0",
"tool_version": "1.0.0",
"message": "Deployed to prod",
"result": { "env": "prod" }
}Error (stderr):
{
"status": "error",
"code": 1,
"error": "user_error",
"message": "flag --env is required",
"suggestion": "pass --env prod",
"recoverable": true,
"schema_version": "1.0"
}Plan (stdout, when --dry-run):
{
"status": "plan",
"schema_version": "1.0",
"message": "Would deploy to prod",
"plan": { "env": "prod" }
}| Code | Constant | Meaning |
|---|---|---|
| 0 | EXIT_OK |
Success |
| 1 | EXIT_USER_ERROR |
Bad input β fix and retry |
| 2 | EXIT_TOOL_ERROR |
Environment / internal failure |
| 3 | EXIT_PARTIAL |
Partial success |
| 4 | EXIT_TIMEOUT |
Timed out β retry after delay |
| 5 | EXIT_NOT_FOUND |
Resource does not exist |
| 6 | EXIT_PERMISSION |
Insufficient permissions |
| 7 | EXIT_CONFLICT |
State conflict |
| 8 | EXIT_RATE_LIMITED |
Rate limited β wait retry_after_ms |
| 9 | EXIT_CANCELLED |
Cancelled |
| Feature | Go | Rust |
|---|---|---|
| Flag injection | Dynamic (runtime) | clap: #[command(flatten)]; argh: separate parse |
| argh flatten | N/A | Not available in argh 0.1.x |
--yes alias |
Sibling flag | visible_alias = "yes" on --force |
enum field |
Enum []string |
enum_values: Vec<String> (reserved word) |
| argh describe | Auto-introspected | Metadata-driven (argh has no runtime tree) |
| Process exit hook | murli.ExitFunc |
write_error_to_streams test helper |
| Profile path | ~/.<tool>/ |
dirs::config_dir()/<tool>/ |
MIT