Skip to content

murli-cli/murli-rs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

15 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

murli 🎢

License: MIT

Rust middleware for CLI tools that makes them speak natively to AI agents β€” with adapters for clap and argh.

Full documentation: murli.allankent.com


Installation

[dependencies]
murli = { version = "0.1", features = ["clap"] }        # clap adapter
murli = { version = "0.1", features = ["argh"] }        # argh adapter
murli = { version = "0.1", features = ["clap", "argh"] } # both

Quick Start β€” clap (derive API)

use 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}));
            }
        }
    }
}

Quick Start β€” clap (builder API)

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
}

Quick Start β€” argh

Note: argh 0.1.x does not support #[argh(flatten)]. Parse GlobalArgs separately 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}));
}

Structured Errors

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).


JSON wire format

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" }
}

Exit codes

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

Deliberate differences from the Go implementation

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>/

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages