murliv1.0.2
{ } github
§ Guides

What you build

These are the calls you write inside your command handlers. murli provides the output methods; you call them with your data.

WriteSuccess

Call at the end of a successful handler. At a TTY it prints the human text; when piped or in agent mode it writes the JSON success envelope with the payload as the result field.

go
results := []map[string]any{
    {"path": "/docs/woodworking", "score": 0.95},
}
w.WriteSuccess("Found 1 result", results)
rust
let results = json!([{"path": "/docs/woodworking", "score": 0.95}]);
writer.write_success("Found 1 result", &results);
python
results = [{"path": "/docs/woodworking", "score": 0.95}]
writer.write_success("Found 1 result", results)

JSON output (all languages):

json
{
  "status": "ok",
  "schema_version": "1.0",
  "tool_version": "1.0.2",
  "message": "Found 1 result",
  "result": [{ "path": "/docs/woodworking", "score": 0.95 }]
}

WriteError

Writes the error envelope to stderr and exits with the code from the error. Do not return from the handler after calling WriteError (Go/Rust) or raising AgentError (Python).

go
if !indexExists {
	w.WriteError(murli.NewUserError(
		"index not found",
		"Run `mytool index build` to create the index.",
	))
	// execution stops here — WriteError calls os.Exit(1)
}
if err != nil {
	w.WriteError(murli.NewToolError(err.Error()))
}
rust
if !index_exists {
    writer.write_error(AgentError::not_found(
        "index not found",
        "Run `mytool index build` to create the index.",
    ));
    // write_error calls process::exit — execution stops here
}
if let Err(e) = result {
    writer.write_error(AgentError::tool_error(&e.to_string()));
}
python
if not index_exists:
    raise murli.AgentError.not_found(
        "index not found",
        "Run `mytool index build` to create the index.",
    )
# For internal errors:
raise murli.AgentError.tool_error(str(err))

WriteEvent — NDJSON streaming

Use for commands that produce output incrementally. Each call writes one JSON object on one line to stdout.

go
for _, chunk := range chunks {
	w.WriteEvent(map[string]any{"event": "chunk", "index": chunk.Index, "data": chunk.Text})
}
w.WriteSuccess("Processed all chunks", summary)
rust
for chunk in &chunks {
    writer.write_event(&json!({"event": "chunk", "index": chunk.index, "data": chunk.text}));
}
writer.write_success("Processed all chunks", &json!(summary));
python
for chunk in chunks:
    writer.write_event({"event": "chunk", "index": chunk.index, "data": chunk.text})
writer.write_success("Processed all chunks", summary)
ndjsonstdout output
{"event":"chunk","index":0,"data":"..."}
{"event":"chunk","index":1,"data":"..."}
{"status":"ok","message":"Processed all chunks","result":{...}}

WriteProgress

Typed progress reporting. In TTY mode written to stderr; in agent mode written as NDJSON to stdout. Use Log / log for simple unstructured messages.

go
for i, item := range items {
	w.WriteProgress(murli.ProgressEvent{
		Stage:   "processing",
		Current: i + 1,
		Total:   len(items),
		Percent: float64(i+1) / float64(len(items)) * 100,
		EtaMs:   estimatedRemainingMs,
		Message: item.Name,
	})
	process(item)
}
w.WriteSuccess("Done", summary)
rust
for (i, item) in items.iter().enumerate() {
    writer.write_progress(ProgressEvent {
        stage:   "processing".into(),
        current: i + 1,
        total:   items.len(),
        percent: (i + 1) as f64 / items.len() as f64 * 100.0,
        message: item.name.clone(),
        ..Default::default()
    });
    process(item);
}
writer.write_success("Done", &json!(summary));
python
for i, item in enumerate(items):
    writer.write_progress(murli.ProgressEvent(
        stage="processing",
        current=i + 1,
        total=len(items),
        percent=(i + 1) / len(items) * 100,
        message=item.name,
    ))
    process(item)
writer.write_success("Done", summary)

WritePlan — dry-run

Use when the handler is running in dry-run mode. Writes a plan envelope and returns cleanly without performing mutations.

go
if w.IsDryRun() {
	w.WritePlan("Would delete 3 files", map[string]any{
		"would_delete": filesToDelete,
		"count":        len(filesToDelete),
	})
	return nil
}
for _, f := range filesToDelete { os.Remove(f) }
w.WriteSuccess("Deleted 3 files", nil)
rust
if writer.is_dry_run() {
    writer.write_plan("Would delete 3 files", &json!({
        "would_delete": files_to_delete,
        "count": files_to_delete.len(),
    }));
    return;
}
for f in &files_to_delete { std::fs::remove_file(f).ok(); }
writer.write_success("Deleted 3 files", &json!(null));
python
if writer.is_dry_run():
    writer.write_plan("Would delete 3 files", {
        "would_delete": files_to_delete,
        "count": len(files_to_delete),
    })
    return
for f in files_to_delete:
    os.remove(f)
writer.write_success("Deleted 3 files", None)

Log

Writes a diagnostic line to stderr. Consecutive identical lines are deduplicated — safe to call in tight loops.

go
w.Log("connecting to index...")
w.Progress("[1/3] reading corpus") // overwrites previous progress line at TTY
rust
writer.log("connecting to index...");
// repeated identical calls are deduplicated
python
writer.log("connecting to index...")
# repeated identical calls are deduplicated

FormatAgentsMD

murli.FormatAgentsMD generates an AGENTS.md stub from the describe output of your CLI. Go only.

gocmd/gen-agents-md/main.go
describeOut := murliCobra.BuildDescribeTree(rootCmd)
agentsMD := murli.FormatAgentsMD(murli.DescribeOutput{
	Name:     rootCmd.Use,
	Commands: []murli.DescribeCommandSchema{describeOut},
})
fmt.Print(agentsMD)

For Rust and Python, use mytool describe --agents-md > AGENTS.md directly from the CLI.