murli

package module
v1.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 29, 2026 License: MIT Imports: 12 Imported by: 0

README

murli 🎶

Go Reference Go Report Card

A pure-Go middleware for CLI tools that makes them speak natively to AI agents — with adapters for spf13/cobra, urfave/cli v2, and urfave/cli v3.

murli is named after Krishna's sacred flute. The murli's music enchants every listener — each feeling it was meant for them alone. This library takes the same approach: your commands don't change, but a human at a terminal gets clear readable output, and an agent reading from a pipe gets structured JSON. Each audience gets the experience shaped for them.


Philosophy

Five principles guide everything murli does:

One tool, two audiences. Humans and agents call the same commands. murli routes output automatically — no if agent { ... } branches in your code.

Discoverability is a first-class feature. Agents shouldn't need documentation to use your tool. The tool describes itself.

Errors are instructions, not messages. A structured error tells an agent what went wrong, whether to retry, and what to do instead.

Dangerous operations require explicit intent. Mutations are rejected in non-interactive mode until the agent — or human — confirms they know what they're doing.

Context windows are finite. Log deduplication, clean stderr routing, and streaming results keep agent context consumption predictable.


Installation

# Core types (Writer, Logger, AgentError, Metadata)
go get github.com/allank/murli

# Pick one adapter for your CLI framework:
go get github.com/allank/murli/cobra     # spf13/cobra
go get github.com/allank/murli/cli/v2   # urfave/cli v2
go get github.com/allank/murli/cli/v3   # urfave/cli v3

Quick Start

The minimal change is one line at startup:

// cobra
_ = murliCobra.Execute(rootCmd)   // replaces rootCmd.Execute()

// urfave/cli v2 or v3
_ = murliCLI.Run(app, os.Args)   // replaces app.Run(os.Args)

That single change gives your tool structured JSON output, --schema on every command, a describe subcommand, a profile subcommand group, and automatic error handling. Everything below layers on top.


Capabilities

Dual-Audience Output

What it is: The same command produces plain text for humans and structured JSON for agents. The switch is automatic — murli checks whether stdout is a terminal.

Principle: One tool, two audiences.


Zero effort — TTY detection is automatic.

Run your command normally: human output. Pipe it: JSON.

$ ./riffle query woodworking
Found 3 matching folders

$ ./riffle query woodworking | cat
{
  "status": "ok",
  "schema_version": "1.0",
  "result": [...]
}

Use --agent to force JSON mode without piping (useful in scripts):

$ ./riffle query woodworking --agent

Write your output once — murli routes it.

Call WriteSuccess and WriteError in your command handlers. murli renders them appropriately for the audience.

RunE: func(cmd *cobra.Command, args []string) error {
    w := murliCobra.NewWriter(cmd)

    results, err := search(args[0])
    if err != nil {
        return murli.NewToolError("search failed: " + err.Error())
    }

    w.WriteSuccess(
        fmt.Sprintf("Found %d results", len(results)), // human text
        results,                                         // agent payload
    )
    return nil
},

Optional — stamp your tool version on every envelope.

Set murli.ToolVersion once at startup (typically via a build-time ldflags variable) and every success envelope carries it:

func main() {
    murli.ToolVersion = version // set via -ldflags "-X main.version=1.2.3"
    _ = murliCobra.Execute(rootCmd)
}
{ "status": "ok", "schema_version": "1.0", "tool_version": "1.2.3", "result": [...] }

Output format — --output.

--output is auto-registered on every command. Pass it to control serialisation:

Value Behaviour
json Pretty-printed JSON (default in agent mode)
ndjson Minified single-line JSON
text Plain text (same as TTY mode)
$ ./riffle query woodworking --output ndjson
{"status":"ok","schema_version":"1.0","result":[...]}

Command Introspection

What it is: Agents can discover everything about your tool — commands, flags, capabilities, and health — without reading documentation. murli auto-mounts three subcommands that do this work.

Principle: Discoverability is a first-class feature.


Zero effort — describe is auto-mounted.

describe dumps your complete command tree as a single JSON document. Agents call it once at startup to understand the full tool.

$ ./riffle describe
{
  "name": "riffle",
  "summary": "Riffle semantic search",
  "schema_version": "1.0",
  "capabilities": {
    "streaming": true,
    "output_formats": ["json", "ndjson", "text"]
  },
  "commands": [
    {
      "name": "query",
      "summary": "Semantic query search",
      "idempotent": true,
      "flags": [...]
    }
  ]
}

Zero effort — --schema is auto-registered on every command.

--schema on any command prints the full schema for that command — flags, arguments, return shape, safety block, and all metadata.

$ ./riffle query --schema
{
  "name": "query",
  "agent_description": "Searches the semantic index for directory conceptual matches.",
  "idempotent": true,
  "arguments": [{ "name": "text", "type": "string", "required": true }],
  "flags": [{ "name": "top", "type": "int", "default": 5 }],
  "safety": { "read_only": true }
}

Positional argument validation is automatically bypassed when generating schemas, so --schema never fails due to missing args.


Development only — doctor requires -tags murlidev.

doctor runs built-in self-checks and reports whether your murli integration is correctly configured. It is a developer tool, not an agent-facing command: in release builds it is stripped so it never appears in describe output or pollutes the command surface agents see.

Build with -tags murlidev during development to enable it:

go build -tags murlidev ./...
go test  -tags murlidev ./...
$ ./riffle doctor --agent
{
  "status": "ok",
  "result": {
    "checks": [
      { "name": "schema_version", "status": "pass" },
      { "name": "output_formats", "status": "pass" },
      { "name": "command_metadata", "status": "warn", "message": "commands missing description: [index]" }
    ],
    "passed": 2, "warnings": 1, "failed": 0
  }
}

status is "ok" when all checks pass or only warnings exist. status is "plan" when any check fails — signalling the tool needs attention before release.


One flag — generate an AGENTS.md stub.

Pass --agents-md to describe to generate a Markdown file ready to drop into your repository. Agents reading your repo get immediate tool context without running the binary.

$ ./riffle describe --agents-md > AGENTS.md
# AGENTS.md

> Auto-generated from `riffle describe`. Edit to add project context.

## Tool: riffle

Riffle semantic search

## Introspection

```bash
riffle describe        # full JSON schema
riffle --help          # human-readable help

Commands

query

Searches the semantic index for directory conceptual matches.

riffle query --schema    # JSON schema for this command

---

### Rich Agent Metadata

**What it is:** Annotations layered onto commands and flags that give agents richer signal — what a command does, when to use it, which flags are sensitive or enumerable, and worked examples. None of it is required; add as much or as little as your tool needs.

**Principle:** Discoverability is a first-class feature.

---

**Zero effort — commands work unannotated.**

murli emits whatever it can infer from your command definitions automatically (name, usage string, flags, argument bounds). Annotation extends that, not replaces it.

---

**One call — annotate a command.**

```go
murliCobra.Annotate(queryCmd, murli.Metadata{
    AgentDescription: "Searches the semantic index for directory conceptual matches.",
    WhenToUse:        "Use when looking for folders matching general topics.",
    Idempotent:       true,
    Returns: &murli.ReturnSchema{
        Type:        "json",
        Description: "Ranked list of vector similarity results",
        Shape:       map[string]any{"path": "string", "score": "float32"},
    },
    Examples: []murli.Example{
        {Command: "riffle query woodworking", Description: "Find woodworking folders"},
        {Command: "riffle query --top 20 art", Description: "Return top 20 art matches"},
    },
})

All fields are optional. Set what is meaningful.


Add code — annotate individual flags.

FlagAnnotations adds per-flag metadata to --schema and describe output, giving agents richer signal for parameter construction:

murliCobra.Annotate(queryCmd, murli.Metadata{
    FlagAnnotations: map[string]murli.FlagAnnotation{
        "region": {
            Env:        "AWS_REGION",
            Enum:       []string{"us-east-1", "eu-west-1", "ap-southeast-1"},
            Persistent: true,
        },
        "token": {
            Env:       "RIFFLE_TOKEN",
            Sensitive: true,  // agents must not log this value
        },
        "top": {
            MutuallyExclusiveWith: []string{"all"},
            Pattern:               `^\d+$`,
        },
    },
})
Field Type Purpose
Env string Environment variable that sets this flag
Sensitive bool Flag carries secrets; agents must not log its value
Persistent bool Flag applies to all subcommands
Enum []string Exhaustive list of valid values
Pattern string Regex the value must match
MutuallyExclusiveWith []string Other flags that cannot be set simultaneously
Profileable bool Flag can be saved in a profile (see Saved Profiles)

Naming convention advisory — requires -tags murlidev.

When built with -tags murlidev, murli emits advisory warnings to stderr in TTY mode when command or flag names deviate from conventional vocabulary:

[murli advisory] command "fetch": prefer "get" (conventional vocabulary)
[murli advisory] flag --format: prefer --output (conventional vocabulary)

Warnings are informational only — they never block execution and are suppressed entirely in agent mode. Common advisories: get over fetch; list over ls; delete over remove; --output over --format. Like doctor, this is a developer aid stripped from release builds.


Structured Errors

What it is: Every error — whether from flag parsing, routing, or your own handler — is intercepted and wrapped into a consistent JSON envelope with an exit code, error type, recovery suggestion, and retryability signal. Agents can act on errors without parsing message strings.

Principle: Errors are instructions, not messages.


Zero effort — flag and routing errors are auto-wrapped.

murli intercepts errors from the CLI framework before your code runs:

$ ./riffle query woodworking --top abc | cat
{
  "code": 1,
  "error": "flag_error",
  "message": "invalid argument \"abc\" for \"--top\" flag",
  "suggestion": "Check command usage with --schema or --help.",
  "recoverable": true,
  "schema_version": "1.0"
}

In TTY mode the same error prints as readable text.


Return structured errors from your handlers.

Use the convenience constructors for common cases:

return murli.NewUserError("query string cannot be empty", "Provide a search keyword.")
return murli.NewToolError("database connection failed: timeout after 30s")

Or build the full AgentError when you need precise control:

return &murli.AgentError{
    Code:        murli.ExitNotFound,
    ErrorType:   "index_missing",
    Message:     "Semantic index not found at ~/.riffle/index",
    Suggestion:  "Run `riffle index build` to create the index first.",
    Recoverable: false,
    DocURL:      "https://example.com/docs/indexing",
}

Extended fields (all optional):

Field Type Purpose
ValidValues []string Enumerable valid inputs when a bad value was supplied
RetryAfterMs int Milliseconds to wait before retrying (use with ExitRateLimited)
DocURL string Link to relevant documentation
Field string Name of the specific flag or argument that caused the error

Exit code taxonomy.

murli standardises exit codes so agents know how to respond to any failure:

Code Constant Meaning Agent action
0 ExitOK Success Proceed
1 ExitUserError Bad input or configuration Read suggestion, fix parameters, retry
2 ExitToolError Environment, network, or filesystem failure Surface to user; do not retry immediately
3 ExitPartial Some operations succeeded, some failed Inspect response, retry on subset if appropriate
4 ExitTimeout Operation timed out Retry after a delay
5 ExitNotFound Requested resource does not exist Verify resource; do not retry blindly
6 ExitPermission Caller lacks permission Not retryable without an auth or config change
7 ExitConflict State conflict (resource already exists, etc.) Read current state before deciding to retry
8 ExitRateLimited Rate limit hit Wait at least retry_after_ms milliseconds
9 ExitCancelled Operation cancelled by signal or context Do not retry unless the parent operation resumes

Context cancellation is handled automatically.

Return ctx.Err() (or any error wrapping it) and murli maps it to the correct exit code and error type — no extra code required:

result, err := doWork(ctx)
if err != nil {
    return fmt.Errorf("work failed: %w", err) // wraps context.Canceled or DeadlineExceeded
}
Returned error Exit code error_type recoverable
context.Canceled 9 (ExitCancelled) "cancelled" false
context.DeadlineExceeded 4 (ExitTimeout) "timeout" true

Safety Rails

What it is: Commands that mutate state are automatically guarded in non-interactive mode. Agents cannot accidentally trigger destructive operations — they must explicitly signal intent. Dry-run support lets agents preview before executing.

Principle: Dangerous operations require explicit intent.


Mark a command mutating — the guard is automatic.

murliCobra.Annotate(deleteCmd, murli.Metadata{
    Mutating:    true,
    Destructive: true,
})

When a mutating command runs in agent mode (non-TTY, no --force), murli rejects it before your handler runs:

{
  "code": 1,
  "error": "confirmation_required",
  "message": "This command mutates state and requires explicit confirmation.",
  "suggestion": "Pass --force or --yes to proceed without a TTY.",
  "recoverable": true
}

--force and --yes are auto-registered on mutating commands. Either bypasses the guard.


The safety block appears automatically in schema output.

Every annotated command carries a safety block in --schema and describe output. Agents use it to reason about risk before calling:

{
  "name": "delete",
  "safety": {
    "read_only": false,
    "idempotent": false,
    "destructive": true,
    "dry_run_supported": true
  }
}

read_only is derived from !Mutating automatically. Fields are omitted from JSON when false.


Add code — support dry-run.

Mark DryRunnable: true and handle the flag in your handler. murli auto-registers --dry-run:

murliCobra.Annotate(deleteCmd, murli.Metadata{
    Mutating:    true,
    DryRunnable: true,
})

deleteCmd.RunE = func(cmd *cobra.Command, args []string) error {
    w := murliCobra.NewWriter(cmd)

    if w.IsDryRun() {
        w.WritePlan("Would delete "+args[0]+" (no changes made)",
            map[string]any{"would_delete": args[0]})
        return nil
    }

    // real deletion here
    w.WriteSuccess("Deleted "+args[0], map[string]any{"id": args[0]})
    return nil
}

WritePlan emits "status": "plan" — same envelope shape as WriteSuccess but signals "preview, not executed." Agents that see "plan" know to confirm before proceeding.


Optional — check IsForced() for your own confirmation logic.

deleteCmd.RunE = func(cmd *cobra.Command, args []string) error {
    w := murliCobra.NewWriter(cmd)
    if !w.IsForced() && isHighRisk(args[0]) {
        return murli.NewUserError("high-risk operation", "Pass --force to confirm.")
    }
    // proceed
}

Saved Profiles

What it is: Named sets of flag values that apply automatically on every invocation. Agents and humans stop repeating --region us-east-1 --token abc on every call — they save a profile once and use it by name.

Principle: One tool, two audiences (agents need persistent configuration too).


Mark flags as profileable — the profile commands are auto-mounted.

// Annotate the root command (or app, for urfave/cli v2)
murliCobra.Annotate(rootCmd, murli.Metadata{
    FlagAnnotations: map[string]murli.FlagAnnotation{
        "region": {Profileable: true},
        "token":  {Profileable: true, Sensitive: true},
    },
})

murli auto-mounts profile save, profile use, profile list, profile show, and profile delete. No further code required.


Save and activate a profile.

# Save the current profileable flag values as "production"
$ mytool --region us-east-1 --token abc123 profile save production

# Set it as the default — all future calls use it automatically
$ mytool profile use production

# Now every invocation gets --region and --token without passing them
$ mytool query woodworking

Pass --profile <name> to override the default for a single invocation. An explicit flag on the command line always wins over a stored profile value. Profiles are stored in ~/.<toolname>/profiles.json.


Agents discover profiles via describe.

{
  "profiles": {
    "available": ["production", "staging"],
    "default": "production",
    "profileable_flags": ["region", "token"]
  }
}

profileable_flags tells agents which flags they can save. available and default tell agents what's already configured.


Streaming & Progress

What it is: Long-running operations — indexing, batch processing, parallel fetches — can stream results and progress incrementally to their audience. Agents get NDJSON on stdout or structured progress objects on stderr; humans get in-place progress lines.

Principle: Context windows are finite.


Stream incremental results with WriteEvent.

WriteEvent is goroutine-safe. Call it as results become available, then close with WriteSuccess or WriteError:

var wg sync.WaitGroup
for _, file := range files {
    wg.Add(1)
    go func(f string) {
        defer wg.Done()
        result := process(f)
        w.WriteEvent(result) // goroutine-safe; one minified JSON line per call
    }(file)
}
wg.Wait()
w.WriteSuccess("Processing complete", nil)

WriteEvent is a no-op in TTY mode — events are machine-only.


Report progress with WriteProgress.

For operations with measurable progress, WriteProgress writes to stderr (keeping stdout clean for results):

w.WriteProgress(murli.ProgressEvent{
    Stage:   "indexing",
    Current: 500,
    Total:   2000,
    Percent: 25.0,
    EtaMs:   6000,
    Message: "Indexing files",
})

Agent mode: minified JSON on stderr. TTY mode: human-readable line with carriage return (overwrites in place). All fields are optional.


Log with deduplication.

w.Progress() and w.Log() write to stderr. In agent mode they produce NDJSON. Consecutive duplicate messages are collapsed with a repeated count, keeping agent context windows lean:

w.Progress("Scanning /docs")
w.Progress("Scanning /docs") // deduplicated
w.Progress("Scanning /docs") // deduplicated
w.Flush()

Agent stderr output:

{"ts":"...","level":"progress","msg":"Scanning /docs","repeated":2}

ANSI escape codes in log messages are automatically stripped in agent mode, keeping JSON entries clean for parsers.


Package Reference

Package Import path Use when
Core github.com/allank/murli Always — Writer, Logger, AgentError, Metadata, schema types
cobra adapter github.com/allank/murli/cobra Your CLI uses spf13/cobra
cli/v2 adapter github.com/allank/murli/cli/v2 Your CLI uses urfave/cli v2
cli/v3 adapter github.com/allank/murli/cli/v3 Your CLI uses urfave/cli v3
conformance github.com/allank/murli/conformance Verify your murli integration satisfies the 1.0 contract in CI

Each adapter exposes the same surface:

Purpose cobra cli/v2 cli/v3
Create writer cobra.NewWriter(cmd) cli.NewWriter(ctx) cli.NewWriter(cmd)
Annotate command cobra.Annotate(cmd, meta) cli.Annotate(cmd, meta) cli.Annotate(cmd, meta)
Annotate root flags cobra.Annotate(rootCmd, meta) cli.AnnotateApp(app, meta) cli.Annotate(app, meta)
Enable + run cobra.Execute(rootCmd) cli.Run(app, os.Args) cli.Run(app, os.Args)
Enable only cobra.Enable(rootCmd) cli.Wrap(app) cli.Wrap(app)
Emit schema cobra.EmitSchema(cmd) cli.EmitSchema(cmd, w) cli.EmitSchema(cmd, w)

Contract Compliance

The murli/conformance package lets downstream CLI maintainers verify their integration against the murli 1.0 contract in CI — without importing murli itself:

func TestConformance(t *testing.T) {
    suite := conformance.NewSuite("/path/to/your/binary")
    suite.Check(t) // verifies describe output, schema_version, capabilities
}

Testing

go test -race ./...                    # release surface (no doctor, no convention checks)
go test -race -tags murlidev ./...     # development surface (doctor command enabled)

License

Distributed under the MIT License. See LICENSE for details.

Documentation

Index

Constants

View Source
const (
	ExitOK        = 0 // Success
	ExitUserError = 1 // Bad input or arguments
	ExitToolError = 2 // Environment/internal failure
	ExitPartial   = 3 // Some operations succeeded, some failed
)

Exit codes 0–3: table-stakes (present since v0.1).

View Source
const (
	ExitTimeout     = 4 // Operation timed out; retry may succeed
	ExitNotFound    = 5 // Requested resource does not exist
	ExitPermission  = 6 // Caller lacks permission; not retryable without auth change
	ExitConflict    = 7 // State conflict (e.g. resource already exists)
	ExitRateLimited = 8 // Rate limit hit; retry after RetryAfterMs
	ExitCancelled   = 9 // Operation cancelled by signal or context
)

Exit codes 4–9: extended taxonomy (v0.2).

View Source
const SchemaVersion = "1.0"

SchemaVersion is the murli envelope schema version. Frozen at "1.0"; incrementing requires a documented migration.

Variables

View Source
var ExitFunc = os.Exit

ExitFunc is swapped out in tests to capture exit codes without terminating.

View Source
var ToolVersion = ""

ToolVersion is the version of the CLI tool using murli. Set this in your main() using a version variable injected at build time:

murli.ToolVersion = version // where `version` is set via -ldflags "-X main.version=1.2.3"

Or inject directly at build time:

-ldflags "-X github.com/allank/murli.ToolVersion=1.2.3"
View Source
var ValidOutputFormats = []string{"json", "ndjson", "text"}

ValidOutputFormats lists the accepted --output values.

View Source
var ValidProtocolVersions = []string{"0.2"}

ValidProtocolVersions lists the accepted --protocol-version values.

Functions

func ApplyFlagAnnotation added in v0.3.0

func ApplyFlagAnnotation(fs *FlagSchema, ann FlagAnnotation)

ApplyFlagAnnotation merges a FlagAnnotation onto a FlagSchema in place. Boolean fields (Sensitive, Persistent, Profileable) are one-way: they can be set to true but not cleared to false. This is intentional — ApplyFlagAnnotation is applied to a zero-value FlagSchema, so false is the default and only needs to be set once. Slice fields (MutuallyExclusiveWith, Enum) are deep-copied to prevent aliasing.

func CheckConventions added in v0.3.0

func CheckConventions(commandNames, flagNames []string, w io.Writer) int

CheckConventions checks command names and flag names against the murli conventional vocabulary. Advisory warnings are written to w. Returns the number of warnings emitted. Never panics; suppressed output is safe (pass io.Discard).

func FormatAgentsMD added in v1.0.0

func FormatAgentsMD(out DescribeOutput) string

FormatAgentsMD generates an AGENTS.md stub from a DescribeOutput. The stub gives agents a quick orientation: tool name and purpose, how to introspect further, and a per-command summary with --schema references. Intended to be pasted as a starting point into the repository's AGENTS.md.

func ProfilePath added in v0.5.0

func ProfilePath(toolName string) string

ProfilePath returns the path to the profiles file for the named tool. Resolves to ~/.{toolName}/profiles.json.

func WriteDoctorTTY added in v1.0.0

func WriteDoctorTTY(w io.Writer, report DoctorReport)

WriteDoctorTTY writes a human-readable doctor report to w. Used by adapter doctor commands when running in a terminal (TTY) context.

Types

type AgentError

type AgentError struct {
	Code          int      `json:"code"`
	ErrorType     string   `json:"error"`
	Message       string   `json:"message"`
	Suggestion    string   `json:"suggestion,omitempty"`
	Recoverable   bool     `json:"recoverable"`
	ValidValues   []string `json:"valid_values,omitempty"`
	RetryAfterMs  int      `json:"retry_after_ms,omitempty"`
	DocURL        string   `json:"doc_url,omitempty"`
	Field         string   `json:"field,omitempty"`
	SchemaVersion string   `json:"schema_version,omitempty"`
	ToolVersion   string   `json:"tool_version,omitempty"`
}

AgentError is the structured error envelope written to stderr. Fields are serialised as JSON in agent mode; SchemaVersion and ToolVersion are auto-populated by WriteError — do not set them manually.

func NewToolError

func NewToolError(message string) *AgentError

NewToolError returns a non-recoverable AgentError for internal/environment failures. Use when the fault is in the environment (network, filesystem, dependency) not the caller.

func NewUserError

func NewUserError(message, suggestion string) *AgentError

NewUserError returns a recoverable AgentError for bad input. Use when the caller supplied invalid arguments or configuration.

func (*AgentError) Error

func (e *AgentError) Error() string

Error implements the standard Go error interface.

type ArgumentMetadata

type ArgumentMetadata struct {
	Name        string `json:"name"`
	Type        string `json:"type"`
	Required    bool   `json:"required"`
	Description string `json:"description"`
}

ArgumentMetadata documents a single positional argument.

type Capabilities added in v0.3.0

type Capabilities struct {
	Streaming       bool     `json:"streaming"`
	DryRun          bool     `json:"dry_run"`
	OutputFormats   []string `json:"output_formats,omitempty"`
	SchemaVersion   string   `json:"schema_version"`
	ToolVersion     string   `json:"tool_version,omitempty"`
	ProtocolVersion string   `json:"protocol_version,omitempty"`
	Profiles        bool     `json:"profiles"`
}

Capabilities describes what the binary supports, auto-populated by murli in describe output.

func DefaultCapabilities added in v0.3.0

func DefaultCapabilities() Capabilities

DefaultCapabilities returns the Capabilities block reflecting the current murli build.

type CheckResult added in v1.0.0

type CheckResult struct {
	Name    string `json:"name"`
	Status  string `json:"status"` // "pass", "warn", or "fail"
	Message string `json:"message,omitempty"`
}

CheckResult is the outcome of one doctor self-check.

type CommandSchema

type CommandSchema struct {
	Name             string             `json:"name"`
	Summary          string             `json:"summary"`
	WhenToUse        string             `json:"when_to_use,omitempty"`
	AgentDescription string             `json:"agent_description,omitempty"`
	Idempotent       bool               `json:"idempotent"`
	Mutating         bool               `json:"mutating,omitempty"`
	Arguments        []ArgumentMetadata `json:"arguments,omitempty"`
	Flags            []FlagSchema       `json:"flags,omitempty"`
	Returns          *ReturnSchema      `json:"returns,omitempty"`
	Examples         []Example          `json:"examples,omitempty"`
	Subcommands      []SubcommandSchema `json:"subcommands,omitempty"`
	Safety           SafetyBlock        `json:"safety"`
}

CommandSchema is the JSON payload emitted by --schema.

type DescribeCommandSchema added in v0.3.0

type DescribeCommandSchema struct {
	Name             string                  `json:"name"`
	Summary          string                  `json:"summary"`
	WhenToUse        string                  `json:"when_to_use,omitempty"`
	AgentDescription string                  `json:"agent_description,omitempty"`
	Idempotent       bool                    `json:"idempotent"`
	Mutating         bool                    `json:"mutating,omitempty"`
	Arguments        []ArgumentMetadata      `json:"arguments,omitempty"`
	Flags            []FlagSchema            `json:"flags,omitempty"`
	Returns          *ReturnSchema           `json:"returns,omitempty"`
	Examples         []Example               `json:"examples,omitempty"`
	Subcommands      []DescribeCommandSchema `json:"subcommands,omitempty"`
	Safety           SafetyBlock             `json:"safety"`
}

DescribeCommandSchema is the full recursive schema used in describe output. Unlike CommandSchema (for --schema), Subcommands here carry full recursive data.

type DescribeOutput added in v0.3.0

type DescribeOutput struct {
	Name          string                  `json:"name"`
	Summary       string                  `json:"summary"`
	SchemaVersion string                  `json:"schema_version"`
	ToolVersion   string                  `json:"tool_version,omitempty"`
	Capabilities  Capabilities            `json:"capabilities"`
	Profiles      *ProfilesInfo           `json:"profiles,omitempty"`
	Commands      []DescribeCommandSchema `json:"commands,omitempty"`
}

DescribeOutput is emitted by the auto-mounted `describe` subcommand. Commands lists the root-level commands of the tool. Each DescribeCommandSchema within Commands has its own Subcommands field for nested commands, creating a recursive tree. The top-level field is named "commands" (not "subcommands") to distinguish root commands from nested subcommands.

type DoctorReport added in v1.0.0

type DoctorReport struct {
	Checks   []CheckResult `json:"checks"`
	Passed   int           `json:"passed"`
	Warnings int           `json:"warnings"`
	Failed   int           `json:"failed"`
}

DoctorReport is the aggregate result of all doctor checks. Embedded in the WriteSuccess result payload by the doctor command.

func RunDoctor added in v1.0.0

func RunDoctor(out DescribeOutput) DoctorReport

RunDoctor runs all self-checks against a DescribeOutput and returns a DoctorReport. Adapters call this after building the DescribeOutput in the doctor command handler.

type Example added in v0.3.0

type Example struct {
	Command          string `json:"command"`
	Description      string `json:"description,omitempty"`
	ExpectedExitCode int    `json:"expected_exit_code,omitempty"` // 0 = success; omitted
}

Example is a concrete usage example for a command, used in Metadata.Examples.

type FlagAnnotation added in v0.3.0

type FlagAnnotation struct {
	Env                   string   `json:"env,omitempty"`
	Sensitive             bool     `json:"sensitive,omitempty"`
	Persistent            bool     `json:"persistent,omitempty"`
	MutuallyExclusiveWith []string `json:"mutually_exclusive_with,omitempty"`
	Enum                  []string `json:"enum,omitempty"`
	Pattern               string   `json:"pattern,omitempty"`
	Profileable           bool     `json:"profileable,omitempty"`
}

FlagAnnotation provides extended metadata for a single flag, keyed by flag name in Metadata.FlagAnnotations. None of these fields can be auto-detected from the CLI framework; all must be supplied by the engineer via Annotate().

type FlagSchema

type FlagSchema struct {
	Name                  string   `json:"name"`
	Type                  string   `json:"type"`
	Default               any      `json:"default"`
	Description           string   `json:"description"`
	Env                   string   `json:"env,omitempty"`
	Sensitive             bool     `json:"sensitive,omitempty"`
	Persistent            bool     `json:"persistent,omitempty"`
	MutuallyExclusiveWith []string `json:"mutually_exclusive_with,omitempty"`
	Enum                  []string `json:"enum,omitempty"`
	Pattern               string   `json:"pattern,omitempty"`
	Profileable           bool     `json:"profileable,omitempty"`
}

FlagSchema represents a single CLI flag in the JSON schema. Basic fields (Name, Type, Default, Description) are auto-populated by the adapter. Extended fields (Env, Sensitive, Persistent, MutuallyExclusiveWith, Enum, Pattern, Profileable) are populated from Metadata.FlagAnnotations[flagName] by the adapter schema emitters (see cobra/schema.go, cli/v2/schema.go, cli/v3/schema.go).

type Logger

type Logger struct {
	// contains filtered or unexported fields
}

Logger writes diagnostic messages to stderr. TTY mode: human-readable plain text. Agent mode: one NDJSON object per line {ts, level, msg}; consecutive duplicates are collapsed into a single entry with a "repeated" count.

func NewLogger

func NewLogger(writer io.Writer, isTTY bool) *Logger

NewLogger initializes a Logger writing to writer.

func (*Logger) Flush

func (l *Logger) Flush()

Flush writes any deferred/deduplicated log entry.

func (*Logger) Log

func (l *Logger) Log(line string)

Log writes a message. In TTY mode: plain text. In agent mode: NDJSON with deduplication. ANSI escape codes are stripped in agent mode.

func (*Logger) LogProgress

func (l *Logger) LogProgress(line string)

LogProgress writes a progress message. TTY: overwrites current line with carriage return. Agent: NDJSON with level "progress" and deduplication. ANSI codes stripped.

type Metadata

type Metadata struct {
	// AgentDescription is a detailed description of the command's scope.
	AgentDescription string `json:"agent_description"`

	// WhenToUse specifies when an agent should select this command.
	WhenToUse string `json:"when_to_use"`

	// Idempotent specifies if the command is safe to re-run on failure.
	Idempotent bool `json:"idempotent"`

	// Mutating marks commands that write, delete, or otherwise change state.
	// When true and the output is not a TTY, the adapter rejects the command with a
	// confirmation_required error to prevent accidental mutation in non-interactive mode.
	// Pass --force or --yes to bypass the guard.
	Mutating bool `json:"mutating,omitempty"`

	// Arguments defines explicit positional argument documentation.
	Arguments []ArgumentMetadata `json:"arguments,omitempty"`

	// Returns defines the expected output format on success.
	Returns *ReturnSchema `json:"returns,omitempty"`

	// Examples contains concrete shell invocations for agent in-context learning.
	// Changed from []string to []Example in v0.3.
	Examples []Example `json:"examples,omitempty"`

	// FlagAnnotations provides extended metadata keyed by flag name, for fields that
	// cannot be auto-detected from the CLI framework (env var binding, enum values, etc.).
	FlagAnnotations map[string]FlagAnnotation `json:"flag_annotations,omitempty"`

	// DryRunnable marks commands that support --dry-run preview mode.
	// When true, murli auto-registers a --dry-run flag on this command.
	// Engineers must check IsDryRun() in their action and call WritePlan() if true.
	DryRunnable bool `json:"dry_runnable,omitempty"`

	// Destructive marks commands whose effects cannot be undone
	// (e.g. delete, overwrite, truncate).
	Destructive bool `json:"destructive,omitempty"`
}

Metadata provides LLM-specific parameters to supplement standard CLI command definitions.

type OutputFormat added in v0.3.0

type OutputFormat string

OutputFormat controls the serialization format of WriteSuccess.

const (
	// OutputFormatDefault is unset — format is determined by TTY detection.
	OutputFormatDefault OutputFormat = ""
	// OutputFormatJSON writes a pretty-printed JSON envelope to stdout.
	OutputFormatJSON OutputFormat = "json"
	// OutputFormatNDJSON writes a minified single-line JSON envelope to stdout.
	OutputFormatNDJSON OutputFormat = "ndjson"
	// OutputFormatText writes human-readable plain text to stdout (same as TTY mode).
	OutputFormatText OutputFormat = "text"
)

type Profile added in v0.5.0

type Profile struct {
	Flags map[string]string `json:"flags"`
}

Profile holds a named set of flag values, stored as strings.

type ProfileStore added in v0.5.0

type ProfileStore struct {
	Default  string             `json:"default,omitempty"`
	Profiles map[string]Profile `json:"profiles"`
}

ProfileStore is the on-disk representation of all saved profiles for a tool.

func LoadProfileStore added in v0.5.0

func LoadProfileStore(toolName string) (*ProfileStore, error)

LoadProfileStore reads the profile store from disk. Returns an empty store (not an error) if the file does not exist.

func (*ProfileStore) Delete added in v0.5.0

func (s *ProfileStore) Delete(name string)

Delete removes a profile. Clears Default if the deleted profile was the default. No-op if the profile does not exist.

func (*ProfileStore) Get added in v0.5.0

func (s *ProfileStore) Get(name string) (Profile, bool)

Get returns the named profile. Returns a zero Profile and false if not found.

func (*ProfileStore) Names added in v0.5.0

func (s *ProfileStore) Names() []string

Names returns all profile names in sorted order.

func (*ProfileStore) Save added in v0.5.0

func (s *ProfileStore) Save(toolName string) error

Save writes the store back to disk, creating the directory if needed.

func (*ProfileStore) Set added in v0.5.0

func (s *ProfileStore) Set(name string, p Profile)

Set adds or replaces a named profile.

func (*ProfileStore) SetDefault added in v0.5.0

func (s *ProfileStore) SetDefault(name string) error

SetDefault marks a profile as the default. Returns an error if the named profile does not exist.

type ProfilesInfo added in v0.5.0

type ProfilesInfo struct {
	Available        []string `json:"available,omitempty"`
	Default          string   `json:"default,omitempty"`
	ProfileableFlags []string `json:"profileable_flags"` // always present; empty array when no flags are profileable
}

ProfilesInfo describes the profile system state for agent consumption. Included in DescribeOutput when the tool supports profiles.

type ProgressEvent

type ProgressEvent struct {
	Stage   string  `json:"stage,omitempty"`
	Current int     `json:"current,omitempty"`
	Total   int     `json:"total,omitempty"`
	Percent float64 `json:"percent,omitempty"`
	EtaMs   int64   `json:"eta_ms,omitempty"`
	Message string  `json:"message,omitempty"`
}

ProgressEvent carries typed progress state for long-running operations. All fields are optional — populate what is meaningful for the operation.

type ReturnSchema

type ReturnSchema struct {
	Type        string         `json:"type"`
	Description string         `json:"description"`
	Shape       map[string]any `json:"shape,omitempty"`
	// OutputSchema carries a raw JSON Schema (draft-2020-12) blob when the engineer
	// provides it via Annotate(). murli serialises it as-is into --schema and describe output.
	OutputSchema json.RawMessage `json:"output_schema,omitempty"`
}

ReturnSchema describes the shape of successful command output.

type SafetyBlock added in v0.4.0

type SafetyBlock struct {
	ReadOnly    bool `json:"read_only"`
	Idempotent  bool `json:"idempotent"`
	Destructive bool `json:"destructive,omitempty"`
	DryRunnable bool `json:"dry_run_supported,omitempty"`
}

SafetyBlock summarises the safety properties of a command for agent consumption. Assembled from Metadata fields by the adapter at schema/describe emit time.

type SubcommandSchema

type SubcommandSchema struct {
	Name    string `json:"name"`
	Summary string `json:"summary"`
}

SubcommandSchema represents a registered subcommand in --schema output (brief listing).

type Writer

type Writer struct {
	// contains filtered or unexported fields
}

Writer handles dynamic output routing based on terminal presence, agent flags, explicit output format, and negotiated protocol version.

func NewWriter

func NewWriter(stdout, stderr io.Writer, agentMode bool, opts ...WriterOption) *Writer

NewWriter returns a configured output writer. Set agentMode true to force JSON output regardless of TTY state. Pass WriterOption values to set OutputFormat or ProtocolVersion.

func (*Writer) Flush

func (w *Writer) Flush()

Flush flushes any deferred deduplicated logs.

func (*Writer) Format added in v0.3.0

func (w *Writer) Format() OutputFormat

Format returns the explicit output format (may be OutputFormatDefault).

func (*Writer) IsDryRun added in v0.4.0

func (w *Writer) IsDryRun() bool

IsDryRun returns true if --dry-run was passed on the command line. Engineers should check this at the start of their action and call WritePlan() if true.

func (*Writer) IsForced added in v0.4.0

func (w *Writer) IsForced() bool

IsForced returns true if --force or --yes was passed on the command line. Engineers may use this to suppress their own confirmation prompts in TTY mode. The non-interactive mutation guard bypass is automatic when IsForced is true.

func (*Writer) IsTTY

func (w *Writer) IsTTY() bool

IsTTY returns true if the writer is in human (TTY) mode.

func (*Writer) Log

func (w *Writer) Log(msg string)

Log writes a message to stderr, deduplicating consecutive duplicates in agent mode.

func (*Writer) Progress

func (w *Writer) Progress(msg string)

Progress writes a progress update; overwrites current line in TTY, collapses in agent mode.

func (*Writer) ProtocolVersion added in v0.3.0

func (w *Writer) ProtocolVersion() string

ProtocolVersion returns the negotiated protocol version (empty string = "0.2").

func (*Writer) WriteError

func (w *Writer) WriteError(err *AgentError)

WriteError writes the structured error to stderr and exits with the error's exit code. In TTY mode: human-readable. In agent mode: JSON envelope with schema_version and tool_version.

func (*Writer) WriteEvent

func (w *Writer) WriteEvent(v any)

WriteEvent writes a single minified JSON object to stdout on one line. It is safe to call WriteEvent concurrently from multiple goroutines. However, WriteSuccess and WriteError are not mutex-protected — call them only after all WriteEvent goroutines have completed (e.g., after a sync.WaitGroup.Wait()). In TTY mode WriteEvent is a no-op (events are machine-only).

func (*Writer) WritePlan added in v0.4.0

func (w *Writer) WritePlan(humanText string, plan any)

WritePlan writes a dry-run plan to stdout. Format depends on outputFormat and isTTY:

  • TTY or OutputFormatText: humanText plain line
  • OutputFormatNDJSON: single minified JSON line with "status": "plan"
  • OutputFormatJSON or default agent mode: pretty-printed JSON with "status": "plan"

func (*Writer) WriteProgress

func (w *Writer) WriteProgress(evt ProgressEvent)

WriteProgress emits a structured progress event to stderr. Agent mode: minified JSON on one line (consistent with WriteEvent). TTY mode: formatted human-readable line with carriage return to overwrite.

func (*Writer) WriteSuccess

func (w *Writer) WriteSuccess(humanText string, jsonPayload any)

WriteSuccess writes to stdout. Format depends on outputFormat and isTTY:

  • TTY or OutputFormatText: humanText plain line
  • OutputFormatNDJSON: single minified JSON line
  • OutputFormatJSON or default agent mode: pretty-printed JSON envelope

type WriterOption added in v0.3.0

type WriterOption func(*Writer)

WriterOption is a functional option for NewWriter.

func WithDryRun added in v0.4.0

func WithDryRun(dryRun bool) WriterOption

WithDryRun sets the dry-run flag. Set to true when --dry-run is present on the command line.

func WithForce added in v0.4.0

func WithForce(force bool) WriterOption

WithForce sets the force flag, activating bypass of the non-interactive mutation guard. Set to true when --force or --yes is present on the command line.

func WithOutputFormat added in v0.3.0

func WithOutputFormat(f OutputFormat) WriterOption

WithOutputFormat sets the explicit output format, overriding TTY auto-detection.

func WithProtocolVersion added in v0.3.0

func WithProtocolVersion(v string) WriterOption

WithProtocolVersion sets the protocol version for envelope shaping. Valid values: "0.1", "0.2". Empty string defaults to "0.2" (current).

Directories

Path Synopsis
cli
v2
v3
Package conformance provides a test suite that verifies a murli-enabled CLI binary satisfies the murli 1.0 contract.
Package conformance provides a test suite that verifies a murli-enabled CLI binary satisfies the murli 1.0 contract.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL