frictionax

package module
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Mar 13, 2026 License: MIT Imports: 19 Imported by: 1

README

FrictionAX

Library for understanding and optimizing for the Agent Experience (AX) - a new category of developer tooling that optimizes CLIs for AI coding agents, not just humans.

FrictionAX is a Go library for CLI friction detection, correction, and telemetry. When users or AI agents type the wrong command, FrictionAX detects the error, suggests corrections, and optionally auto-executes high-confidence fixes. FrictionAX events are collected and forwarded to your own backend for analysis, revealing desire paths — patterns where users consistently expect something that doesn't exist yet. Watch how we discovered agents wanted a query command →

FrictionAX is a client-side library. You provide the collection endpoint, dashboard, and storage — FrictionAX handles the detection, suggestion, redaction, and buffered upload.

This was originally extracted from SageOx's ox CLI.

See it in action: The Hive is Buzzing — how we use friction to fine-tune CLIs for coding agents.

friction dashboard

The problem

CLIs fail hard on unknown commands:

Error: unknown command "agent-list"
Run 'mycli --help' for usage.

FrictionAX turns failure into guidance:

Error: unknown command "agent-list"

Did you mean?
    mycli agent list

Coding agents (Claude Code, Cursor, Copilot) frequently hallucinate CLI commands. FrictionAX detects intent, suggests the right command, and teaches the agent the correct syntax — without permanent aliases cluttering the interface.

Install

go get github.com/sageox/frictionax

Quick start

package main

import (
    "os"

    "github.com/sageox/frictionax"
    frictioncobra "github.com/sageox/frictionax/adapters/cobra"
    "github.com/spf13/cobra"
)

func main() {
    root := &cobra.Command{Use: "mycli"}
    // ... add subcommands ...

    adapter := frictioncobra.NewCobraAdapter(root)
    f := friction.New(adapter,
        friction.WithCatalog("mycli"),
    )
    defer f.Close()

    if err := root.Execute(); err != nil {
        result := f.Handle(os.Args[1:], err)
        if result != nil {
            result.Emit(false) // human-friendly output
        }
        os.Exit(1)
    }
}

How it works

flowchart TD
    A["User types: mycli agent prine"] -->|error| B[friction.Handle]
    B --> C{Suggestion chain}
    C -->|1. Catalog remap| D[Full command remap]
    C -->|2. Token fix| E[Single token correction]
    C -->|3. Levenshtein| F[Edit-distance guess]
    D & E & F --> G{Confidence >= 0.85<br/>and auto_execute?}
    G -->|yes| H[Auto-execute corrected command]
    G -->|no| I["Show 'did you mean?'"]
    H & I --> J[Record friction event]

Suggestion chain

Suggestions are tried in priority order:

  1. Catalog remap — Full command remaps from learned patterns (highest confidence)
  2. Token fix — Single-token corrections from the catalog
  3. Levenshtein — Edit-distance fallback for typos (never auto-executed)

Adapters

FrictionAX works with any CLI framework via the CLIAdapter interface. Built-in adapters:

Adapter Import
Cobra github.com/sageox/frictionax/adapters/cobra
Kong github.com/sageox/frictionax/adapters/kong
urfave/cli github.com/sageox/frictionax/adapters/urfavecli
Custom adapter

Implement the CLIAdapter interface:

type CLIAdapter interface {
    CommandNames() []string
    FlagNames(command string) []string
    ParseError(err error) *ParsedError
}

Options

f := friction.New(adapter,
    friction.WithCatalog("mycli"),                              // enable learned corrections
    friction.WithTelemetry("https://api.example.com", "1.0"),   // report friction events
    friction.WithAuth(func() string { return token }),          // bearer token for telemetry
    friction.WithRedactor(secrets.New()),                       // redact secrets from events
    friction.WithActorDetector(myDetector),                     // custom human/agent detection
    friction.WithCachePath("/tmp/mycli-catalog.json"),          // persist catalog to disk
    friction.WithIsEnabled(func() bool { return true }),        // toggle telemetry
)
defer f.Close()

Telemetry

FrictionAX buffers friction events in-process and periodically sends them to your endpoint via POST. You build and host the collection service — FrictionAX handles the client side.

Events are sent as JSON batches:

{"v": "1.0", "events": [{"ts": "...", "kind": "unknown-command", "command": "agent", "actor": "agent", "input": "mycli agent-list", "error_msg": "unknown command \"agent-list\"", "suggestion": "mycli agent list"}]}

The collector supports:

  • Buffered batching — events accumulate in a ring buffer and flush periodically or when a threshold is reached
  • Rate limiting — respects X-Friction-Sample-Rate and Retry-After headers from your server
  • Catalog sync — your server can return updated catalog data in the response, which FrictionAX caches locally

The dashboard screenshot above is from our own service built on top of this data. What you build with the events is up to you.

Sample collection server

The repo includes a working example server (cmd/friction-server) that collects events into SQLite and serves a basic dashboard — useful for local development or as a reference for building your own:

go run github.com/sageox/frictionax/cmd/friction-server@latest --port=8080 --db=./friction.db
Endpoint Method Description
/api/v1/friction POST Submit friction events
/api/v1/friction/status GET Health and event count
/api/v1/friction/summary GET Aggregated patterns
/api/v1/friction/catalog GET/PUT Manage catalog
/dashboard GET HTML dashboard

Agent output

FrictionAX formats output differently for agents vs humans:

Human (stderr):

Did you mean?
    mycli agent list

Agent (stdout JSON):

{"_corrected": {"was": "agent-list", "now": "agent list", "note": "Use 'mycli agent list' next time"}}

Agents see corrections in their context and learn the correct syntax for subsequent calls.

Secret redaction

Built-in redactor with 25+ patterns (AWS, GitHub, Slack, Stripe, JWTs, connection strings):

import "github.com/sageox/frictionax/redactors/secrets"

f := friction.New(adapter, friction.WithRedactor(secrets.New()))

Privacy

  • Secrets redacted via pluggable Redactor interface
  • File paths bucketed to categories, not captured verbatim
  • Error messages truncated and sanitized
  • No user identity or repository names captured

AX Toolchain

FrictionAX is part of the Agent Experience (AX) toolchain — open-source libraries for building CLIs that work well with AI coding agents:

  • FrictionAX — Friction detection, correction, and telemetry for Agent Experience (this repo)
  • AgentX — Detect which coding agent is calling your CLI and adapt behavior accordingly
  • ox — The CLI where the AX toolchain was born

Contributing

PRs welcome — see CONTRIBUTING.md.

License

MIT — see LICENSE.

Documentation

Overview

Package frictionax provides CLI friction detection, correction, and telemetry for any command-line tool.

It detects CLI usage errors (typos, unknown commands, unknown flags) and provides helpful corrections with optional auto-execution for high-confidence matches. Designed to work with any CLI framework via pluggable adapters.

Core Concepts

  • FrictionEvent: Captures a CLI usage failure for analytics
  • Suggestion: Represents a correction suggestion with confidence score
  • Handler: Processes CLI errors and generates suggestions
  • Catalog: Stores learned command/token mappings for high-confidence corrections

Suggestion Chain Priority

When handling a CLI error, suggestions are tried in this order:

  1. Full command remap from catalog (highest confidence)
  2. Token-level catalog lookup
  3. Levenshtein distance fallback (for typos)

Auto-Execute

High-confidence catalog matches with AutoExecute=true can be automatically executed without user confirmation. This enables "desire path" support where common patterns are seamlessly corrected.

Actor Detection

The system distinguishes between human and agent actors to:

  • Format output appropriately (text vs JSON)
  • Track analytics by actor type
  • Adjust behavior based on actor patterns

Actor detection is pluggable via the ActorDetector interface. The default EnvActorDetector checks CI environment variables, but consumers can inject their own detection logic (e.g., checking for specific agent frameworks).

Redaction

Input redaction is pluggable via the Redactor interface. Consumers provide their own redaction logic for secrets and sensitive data. A NoOpRedactor is included for cases where redaction is not needed.

Privacy Guarantees

  • Secrets are redacted via the pluggable Redactor interface
  • File paths are bucketed to categories, not captured
  • Error messages are truncated and sanitized
  • No user identity or repository names captured

Auto-Execute Philosophy

Not every correction is auto-executed. Only curated catalog entries with:

  • auto_execute: true flag set
  • Confidence >= 0.85 threshold
  • Safe, non-destructive operations

Levenshtein suggestions are NEVER auto-executed (they're typo guesses, not expressions of intent).

Teaching Pattern

When a command is corrected, the correction is emitted in stdout (not stderr) so agents see it in their context and learn for subsequent calls.

Desire Paths

A "desire path" is a reasonable expectation that doesn't match current behavior. When many users/agents make the same "mistake," that's a signal the CLI should work that way. This package surfaces these patterns through analytics.

Created by SageOx (https://sageox.ai).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Actor

type Actor string

Actor identifies who initiated the command (human user or AI agent).

const (
	// ActorHuman indicates a human user typed the command.
	ActorHuman Actor = "human"

	// ActorAgent indicates an AI coding agent generated the command.
	ActorAgent Actor = "agent"

	// ActorUnknown indicates the actor could not be determined.
	ActorUnknown Actor = "unknown"
)

type ActorDetector

type ActorDetector interface {
	DetectActor() (Actor, string)
}

ActorDetector detects whether the current context is a human, AI agent, or CI.

type BuildConfig

type BuildConfig struct {
	MinHumanCount int
	MinAgentCount int
	MinTotalCount int
	SkipKinds     []string
	DiffOnly      bool
}

BuildConfig controls the catalog build process.

func DefaultBuildConfig

func DefaultBuildConfig() BuildConfig

DefaultBuildConfig returns sensible defaults for catalog building.

type BuildResult

type BuildResult struct {
	Catalog    CatalogData      `json:"catalog"`
	NewEntries []CommandMapping `json:"new_entries"`
	Skipped    []SkippedPattern `json:"skipped"`
}

BuildResult contains the output of a catalog build.

func Build

func Build(patterns []PatternDetail, existing CatalogData, cfg BuildConfig) (*BuildResult, error)

Build creates catalog entries from pattern data, deduplicating against an existing catalog. It filters patterns by thresholds, skips configured kinds, and produces CommandMapping entries with empty Target fields (targets are determined by LLM reasoning in the skill layer).

type CLIAdapter

type CLIAdapter interface {
	// CommandNames returns all available command names.
	CommandNames() []string

	// FlagNames returns all available flag names for a command.
	// If command is empty, returns global flags.
	FlagNames(command string) []string

	// ParseError extracts structured info from a CLI error.
	// Returns nil if the error is not a parseable CLI error.
	ParseError(err error) *ParsedError
}

CLIAdapter abstracts CLI framework details for error parsing. Implement this for your CLI framework (Cobra, urfave/cli, Kong, etc.).

type CatalogData

type CatalogData struct {
	Version  string           `json:"version"`
	Commands []CommandMapping `json:"commands"`
	Tokens   []TokenMapping   `json:"tokens"`
}

CatalogData contains all learned mappings for serialization. This is the wire format for catalog updates from the server.

type CommandMapping

type CommandMapping struct {
	Pattern     string  `json:"pattern"`
	Target      string  `json:"target"`
	HasRegex    bool    `json:"has_regex"`
	AutoExecute bool    `json:"auto_execute"`
	Count       int     `json:"count"`
	Confidence  float64 `json:"confidence"`
	Description string  `json:"description"`
	// contains filtered or unexported fields
}

CommandMapping represents a full command remap from learned patterns.

func (*CommandMapping) ApplyMapping

func (m *CommandMapping) ApplyMapping(input string) (string, bool)

ApplyMapping applies the mapping to input, returning corrected command. For regex patterns, captures are substituted into target ($1, $2, etc). Returns the corrected command and true if matched, empty string and false otherwise.

type FailureKind

type FailureKind string

FailureKind categorizes CLI usage failures for analytics and suggestion routing.

const (
	// FailureUnknownCommand indicates an unknown command was entered.
	FailureUnknownCommand FailureKind = "unknown-command"

	// FailureUnknownFlag indicates an unknown flag was provided.
	FailureUnknownFlag FailureKind = "unknown-flag"

	// FailureMissingRequired indicates a required argument or flag is missing.
	FailureMissingRequired FailureKind = "missing-required"

	// FailureInvalidArg indicates an argument has an invalid value.
	FailureInvalidArg FailureKind = "invalid-arg"

	// FailureParseError indicates a general CLI parsing failure.
	FailureParseError FailureKind = "parse-error"
)

type FileSource

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

FileSource reads patterns from a JSON file or stdin.

func NewFileSource

func NewFileSource(path string) *FileSource

NewFileSource creates a PatternSource that reads from a JSON file. Use "-" to read from stdin.

func (*FileSource) FetchPatterns

func (s *FileSource) FetchPatterns(ctx context.Context, minCount int, limit int) ([]PatternDetail, error)

FetchPatterns reads patterns from the file, applying minCount and limit filters.

type Friction

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

Friction is the single entry point for CLI friction detection, correction, and telemetry. Create one with New() and configure via options.

Basic usage (suggestions only, no telemetry):

f := frictionax.New(adapter)
result := f.Handle(args, err)

Full usage (suggestions + telemetry):

f := frictionax.New(adapter,
    frictionax.WithCatalog("mycli"),
    frictionax.WithTelemetry("https://api.example.com", "0.5.0"),
    frictionax.WithAuth(func() string { return token }),
)
defer f.Close()
result := f.Handle(args, err)

func New

func New(adapter CLIAdapter, opts ...Option) *Friction

New creates a Friction instance with the given adapter and options. The adapter is required and provides CLI framework integration.

Call Close() when done to flush any buffered telemetry events.

func (*Friction) Close

func (f *Friction) Close()

Close stops background telemetry processing and flushes buffered events. Safe to call multiple times. No-op if telemetry is not configured.

func (*Friction) Handle

func (f *Friction) Handle(args []string, err error) *Result

Handle processes CLI args and error, returning a Result with suggestion and execution decision. Returns nil if the error cannot be parsed.

func (*Friction) Record

func (f *Friction) Record(event FrictionEvent)

Record adds a friction event directly to the telemetry buffer. This is useful for recording events that aren't CLI parse errors. No-op if telemetry is not configured.

func (*Friction) Stats

func (f *Friction) Stats() Stats

Stats returns current telemetry statistics. Returns zero-value Stats if telemetry is not configured.

func (*Friction) UpdateCatalog

func (f *Friction) UpdateCatalog(data CatalogData) error

UpdateCatalog replaces the catalog data with the provided data. No-op if the catalog is not configured.

type FrictionEvent

type FrictionEvent struct {
	Timestamp  string      `json:"ts"`
	Kind       FailureKind `json:"kind"`
	Command    string      `json:"command,omitempty"`
	Subcommand string      `json:"subcommand,omitempty"`
	Actor      string      `json:"actor"`
	AgentType  string      `json:"agent_type,omitempty"`
	PathBucket string      `json:"path_bucket"`
	Input      string      `json:"input"`
	ErrorMsg   string      `json:"error_msg"`
	Suggestion string      `json:"suggestion,omitempty"`
}

FrictionEvent captures a CLI usage failure for analytics. Events are privacy-preserving: inputs are redacted, errors are truncated.

Field limits:

  • Input: max 500 characters
  • ErrorMsg: max 200 characters

Use Truncate() to enforce these limits before submission.

func (*FrictionEvent) MarshalJSON

func (f *FrictionEvent) MarshalJSON() ([]byte, error)

MarshalJSON returns JSON bytes ready for transmission.

func (*FrictionEvent) Truncate

func (f *FrictionEvent) Truncate()

Truncate enforces field length limits on the event. Call this before submission to ensure compliance with API limits.

type FrictionResponse

type FrictionResponse struct {
	Accepted int          `json:"accepted"`
	Catalog  *CatalogData `json:"catalog,omitempty"`
}

FrictionResponse represents the API response from friction event submission.

type HTTPSource

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

HTTPSource fetches patterns from a frictionax-server HTTP endpoint.

func NewHTTPSource

func NewHTTPSource(baseURL string) *HTTPSource

NewHTTPSource creates a PatternSource that fetches from the given frictionax-server URL. The server must expose GET /api/v1/friction/patterns.

func (*HTTPSource) FetchPatterns

func (s *HTTPSource) FetchPatterns(ctx context.Context, minCount int, limit int) ([]PatternDetail, error)

FetchPatterns fetches patterns from the HTTP endpoint.

type Option

type Option func(*frictionConfig)

Option configures the Friction instance.

func WithActorDetector

func WithActorDetector(d ActorDetector) Option

WithActorDetector sets a custom actor detector. If not set, the default environment-based detector is used.

func WithAuth

func WithAuth(fn func() string) Option

WithAuth sets the authentication function for telemetry requests. The function is called on each request and should return a bearer token.

func WithCachePath

func WithCachePath(path string) Option

WithCachePath sets the file path for on-disk catalog caching. If not set, no catalog caching is performed.

func WithCatalog

func WithCatalog(cliName string) Option

WithCatalog enables the learned-corrections catalog for the given CLI name. The catalog provides high-confidence corrections from server-side data.

func WithIsEnabled

func WithIsEnabled(fn func() bool) Option

WithIsEnabled sets a function that controls whether telemetry is active. If the function returns false, events are silently dropped.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger sets a structured logger for debug output. If not set, slog.Default() is used.

func WithRedactor

func WithRedactor(r Redactor) Option

WithRedactor sets a custom redactor for input and error message sanitization. If not set, inputs are passed through unmodified.

func WithRequestDecorator

func WithRequestDecorator(fn func(*http.Request)) Option

WithRequestDecorator sets a function called on each outgoing HTTP request. Use this to add custom headers (e.g., User-Agent).

func WithTelemetry

func WithTelemetry(endpoint, version string) Option

WithTelemetry enables background telemetry reporting. Events are buffered and sent to the endpoint periodically.

type ParsedError

type ParsedError struct {
	Kind       FailureKind
	BadToken   string
	Command    string
	Subcommand string
	RawMessage string
}

ParsedError contains structured error information extracted from CLI parsing. CLIAdapter implementations parse raw errors into this structure.

type PatternDetail

type PatternDetail struct {
	Pattern       string `json:"pattern"`
	Kind          string `json:"kind"`
	ErrorMsg      string `json:"error_msg,omitempty"`
	TotalCount    int64  `json:"total_count"`
	HumanCount    int64  `json:"human_count"`
	AgentCount    int64  `json:"agent_count"`
	AgentTypes    int64  `json:"agent_types"`
	LatestVersion string `json:"latest_version,omitempty"`
	FirstSeen     string `json:"first_seen"`
	LastSeen      string `json:"last_seen"`
}

PatternDetail represents an aggregated friction pattern with actor breakdown. This is the wire format for pattern aggregation APIs.

type PatternSource

type PatternSource interface {
	FetchPatterns(ctx context.Context, minCount int, limit int) ([]PatternDetail, error)
}

PatternSource fetches aggregated friction patterns from a data source.

type PatternsResponse

type PatternsResponse struct {
	Patterns []PatternDetail `json:"patterns"`
	Total    int             `json:"total"`
}

PatternsResponse wraps pattern details for API responses.

type Redactor

type Redactor interface {
	Redact(input string) string
}

Redactor redacts sensitive information from strings.

type Result

type Result struct {
	// Suggestion contains the correction suggestion (may be nil if no suggestion found).
	Suggestion *Suggestion

	// Event contains the friction event for analytics submission.
	Event *FrictionEvent

	// AutoExecute is true if the corrected command should be auto-executed.
	// Only true for high-confidence catalog matches.
	AutoExecute bool

	// CorrectedArgs contains the args to re-execute with (if AutoExecute is true).
	CorrectedArgs []string
}

Result contains the outcome of friction handling. It includes the suggestion, friction event for analytics, and execution decision.

func (*Result) Emit

func (r *Result) Emit(jsonMode bool)

Emit outputs the correction/suggestion for the caller to learn from. For agents (jsonMode=true): writes structured JSON to stdout. For humans (jsonMode=false): writes human-friendly text to stderr.

func (*Result) EmitError

func (r *Result) EmitError(msg string, jsonMode bool)

EmitError outputs an error with a suggestion for agent learning. For suggest-only cases where the command is not auto-executed.

func (*Result) Format

func (r *Result) Format(jsonMode bool) string

Format formats the result's suggestion for output. If jsonMode is true, returns a JSON object with type, suggestion, and confidence. Otherwise returns human-friendly text ("Did you mean this?\n <suggestion>"). Returns empty string if no suggestion is present.

func (*Result) WrapOutput

func (r *Result) WrapOutput(output any) any

WrapOutput wraps successful command output with correction metadata. This teaches agents what the correct command is for next time.

type SkippedPattern

type SkippedPattern struct {
	Pattern string `json:"pattern"`
	Kind    string `json:"kind"`
	Reason  string `json:"reason"`
}

SkippedPattern records why a pattern was not added to the catalog.

type Stats

type Stats struct {
	Enabled        bool    `json:"enabled"`
	BufferCount    int     `json:"buffer_count"`
	BufferSize     int     `json:"buffer_size"`
	SampleRate     float64 `json:"sample_rate"`
	RetryAfter     any     `json:"retry_after"`
	CatalogVersion string  `json:"catalog_version,omitempty"`
}

Stats holds telemetry statistics for status display.

type SubmitRequest

type SubmitRequest struct {
	Version string          `json:"v"`
	Events  []FrictionEvent `json:"events"`
}

SubmitRequest is the request body for friction event submission.

type Suggestion

type Suggestion struct {
	Type        SuggestionType
	Original    string
	Corrected   string
	Confidence  float64
	Description string
}

Suggestion represents a correction suggestion for a CLI error.

type SuggestionType

type SuggestionType string

SuggestionType indicates the source/method used to generate a suggestion.

const (
	// SuggestionCommandRemap indicates a full command remap from the catalog.
	SuggestionCommandRemap SuggestionType = "command-remap"

	// SuggestionTokenFix indicates a single token correction from the catalog.
	SuggestionTokenFix SuggestionType = "token-fix"

	// SuggestionLevenshtein indicates an edit-distance based guess.
	SuggestionLevenshtein SuggestionType = "levenshtein"
)

type TokenMapping

type TokenMapping struct {
	Pattern    string      `json:"pattern"`
	Target     string      `json:"target"`
	Kind       FailureKind `json:"kind"`
	Count      int         `json:"count"`
	Confidence float64     `json:"confidence"`
}

TokenMapping represents a single-token correction from learned patterns.

Directories

Path Synopsis
adapters
cobra
Package cobra implements a frictionax.CLIAdapter for the spf13/cobra CLI framework.
Package cobra implements a frictionax.CLIAdapter for the spf13/cobra CLI framework.
kong
Package kong implements a frictionax.CLIAdapter for the alecthomas/kong CLI framework.
Package kong implements a frictionax.CLIAdapter for the alecthomas/kong CLI framework.
urfavecli
Package urfavecli implements a frictionax.CLIAdapter for the urfave/cli/v2 framework.
Package urfavecli implements a frictionax.CLIAdapter for the urfave/cli/v2 framework.
cmd
frictionax command
internal
levenshtein
Package levenshtein provides edit distance calculation between strings.
Package levenshtein provides edit distance calculation between strings.
ringbuffer
Package ringbuffer provides a generic thread-safe circular buffer with deduplication.
Package ringbuffer provides a generic thread-safe circular buffer with deduplication.
throttle
Package throttle provides a flush rate limiter to prevent thundering herd.
Package throttle provides a flush rate limiter to prevent thundering herd.
redactors
secrets
Package secrets implements a frictionax.Redactor with built-in secret detection patterns.
Package secrets implements a frictionax.Redactor with built-in secret detection patterns.

Jump to

Keyboard shortcuts

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