agent-cli-kit

module
v0.9.0 Latest Latest
Warning

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

Go to latest
Published: Nov 6, 2026 License: MIT

README

agent-cli-kit

Shared Go packages for the AngelMsger CLI family: jira-cli, confluence-cli, bitbucket-cli, jenkins-cli, prometheus-cli, openobserve-cli, wecom-calendar-cli and wecom-channel-cli. Each CLI used to carry its own copy of these packages, and the copies drifted. The kit keeps one copy, so a fix such as the macOS keychain probe lands once.

The kit holds code only. The family's principles and rules for building CLIs that work well for both people and agents live in agent-clis-workspace.

go get github.com/angelmsger/agent-cli-kit@latest

Packages

Package What it does
errors Categorized CLIError, exit codes, HTTP status mapping and the JSON error envelope agents parse. Also the family's write-outcome rule (FromWrite, WriteSucceeded), its redirect rule (WithRedirect) and its one READONLY_BLOCKED wording (ReadOnlyBlocked).
output JSON and table rendering, --fields projection, the list envelope with pagination and _notice.
transport HTTP client with decorators, bounded retries that replay the request body, and optional --verbose logging.
credstore Secret storage in the OS keychain with a protected file fallback. Probes macOS for a default keychain first. Lookup reports what each backend answered.
auth Auth schemes, the Authorization header, keychain account keys, and Resolve, which finds a secret and returns recovery errors an agent can act on when the sandbox cannot see it.
config Layered settings (flags, environment, .env, a YAML file of named contexts, defaults) with the layer that supplied each value, context selection, the atomic file write, team presets (set-context), and the context hints.
cliflags Corrects common agent mistakes in argv (--flag value vs --flag=value, literal \n in body flags).
update Cached check against a repository's latest GitHub release.
skill Read-only Skill version, load-state and installation inspection over caller-supplied fs.FS, plus agent integration directory metadata.
terminal Whether a stream is an interactive terminal (Is), and prompts that read and write the command's own streams (Prompter: line, choice, confirm, hidden secret).
doctor How a doctor check's error becomes its status (ok, rejected_by_server, redirected, unreachable …), recovery scope and detail. The report and its next steps stay with each CLI.
reuse The auth reuse decision: which stored login a context adopts, in what order, on what evidence and within what deadline. A CLI describes its contexts and supplies resolution, verification and the config write.

What each CLI supplies

The kit has no business CLI names, environment variables or commands in its implementation. Shared Skill integration IDs (such as codex) identify supported agent directories; they do not select product behavior. A CLI passes its product settings in once, at startup:

// Product hints and next steps for each error category.
errors.SetGuidance(hints.Guidance)

client := transport.New(transport.Options{
	UserAgent:  "jenkins-cli/" + constants.Version,
	MaxRetries: 2,
	Verbose:    verboseWriter,
})

store := credstore.NewStore(configDir, "jenkins-cli")

// Auth schemes and the product wording of the recovery steps.
auth.Register(&auth.Product{
	Schemes: []auth.Scheme{
		{Name: "token", Kind: auth.Basic, MissingCode: "AUTH_NO_TOKEN",
			Missing: "token auth requires both a username and an API token"},
		{Name: "basic", Kind: auth.Basic, Missing: "basic auth requires both a username and a password"},
	},
	DefaultScheme:    "token",
	Name:             "Jenkins",
	Doctor:           "jenkins-cli doctor",
	MissingStep:      "Only if the host retry also reports missing credentials, run `jenkins-cli config init` in the user's terminal or set JENKINS_* environment variables.",
	InaccessibleStep: "Do not run `config init` unless the same check also fails in the host environment.",
	ListSchemes:      true,
})
cred, err := auth.Resolve(auth.Request{
	Scheme:   cfg.Auth.Scheme,
	Username: cfg.Auth.Username,
	Secrets:  map[string]string{"token": secrets.Token, "basic": secrets.Password},
	BaseURL:  cfg.BaseURL,
}, store)

// Settings: one row per field, the file location and the command wording.
// The CLI keeps its own structs, tagged `config:"<key>"`, and its YAML shape.
config.Register(&config.Schema{
	Fields: []config.Field{
		{Key: config.KeyServer, Env: []string{"JENKINS_URL"}, Service: true},
		{Key: config.KeyAuthScheme, Env: []string{"JENKINS_AUTH_SCHEME"}, Default: "token", Service: true},
		{Key: "auth.username", Env: []string{"JENKINS_USER"}},
		{Key: "defaults.timeout", Default: "30s"},
		{Key: "secret.token", Env: []string{"JENKINS_TOKEN"}, Implies: "token"},
	},
	Paths: config.Paths{Dir: ".angelmsger/jenkins", File: "config.yaml"},
	Words: config.Words{App: "jenkins-cli", ContextFlag: "--use-context", ContextEnv: "JENKINS_CONTEXT",
		NoHintEnv: "JENKINS_CLI_NO_CONTEXT_HINT", ListContexts: "config get-contexts",
		UseContext: "config use-context", SetContext: "config set-context",
		AuthGuide: "auth guide", AuthLogin: "auth login"},
	Schemes: []string{"token", "basic"},
	URL:     config.URLRules{AssumeScheme: "http"},
})
var shape fileShape // the CLI's own YAML document type
found, err := config.ReadFile(dir, &shape)
file := shape.file() // a config.File[NamedContext, Defaults]
resolved, err := config.Load(file, config.LoadOptions{Flags: config.Encode(flags), Context: useContext})
config.Decode(resolved.Values, &cfg)

checker := update.Checker{Repo: "AngelMsger/jenkins-cli", EndpointEnv: "JENKINS_RELEASE_API"}
status := checker.Cached(ctx, client, cacheDir, constants.Version)

Output integration

For paginated NDJSON, output.EmitList writes item rows first, then emits one compact _notice.pagination record only if more results remain and every row write succeeded. Empty filtered pages and field projections retain continuation; completed pages and stdout failures emit no pagination notice. JSON envelopes remain unchanged. Options.NoticeWriter defaults to stderr; Options.NextFlag defaults to --cursor and controls both the NDJSON hint and table footer. Commands supply their actual cursor/offset and continuation flag. The family wire contract defines observable behavior; package tests pin serialization and ordering.

Write outcomes

A write that fails may still have been applied, so repeating it can apply it twice. The family rule is that an uncertain write is reconciled with a read first. A CLI passes each write's error through errors.FromWrite with a WriteTarget: what the write changes, and the bounded read commands that show it (quote user values with errors.ShellArg, for POSIX shells). Only a definite refusal (usage, config, auth, permission, not found, conflict) comes back unchanged. Anything else, including an error that is not a CLIError, comes back as "Write outcome is unknown for …": not retryable, the hint "Do not repeat the write", and the read commands as next steps. A 2xx response that could not be decoded, errors.MissingWriteIdentity (no ID or queue location in the response) and errors.WriteSucceeded (a later read failed) report a write that was applied but whose result cannot be shown, under WRITE_SUCCEEDED_* codes that keep the identity of what was changed. An error that already reports a write outcome passes through FromWrite unchanged, so a client decorator can classify every write without nesting one outcome inside another.

Redirects

The default Doer returns a redirect instead of following it when following would answer a different request (any write, or a read whose method a 301, 302 or 303 would turn into a GET), when the hop leaves the origin under SameOriginRedirects, or at the tenth hop. Each comes back as the answer, with its status and Location, never as a network error, and is not retried. A custom Options.Doer keeps its own policy: a redirect it refuses comes back with its error, and a read may be retried after it. A CLI builds its error for any 3xx with WithRedirect(location, errors.RedirectGuide{Setting, Steps}), where the location is resp.Location() (nil when there is none). The error keeps the CLI's code, category and HTTP status, is not retryable (the same request is redirected again), and gets one hint for the family: where the redirect points (scheme and host only; a path can carry a session ID and a query a sign-in token), what a redirect usually means (a login page, a single sign-on proxy, or a server URL that should use https), and the CLI's setting to check. A 3xx without a Location (a 300 or 304) gets a hint that the same request gets the same answer, naming the error's HTTP status, so set that first. FromWrite still gives a write's redirect its own hint and read steps.

A CLI's doctor maps each check's error with doctor.Status, doctor.RecoveryScope and doctor.Detail, so a redirect reads as redirected, with the hint in its detail, rather than unreachable.

Read-only mode and prompts

errors.ReadOnlyBlocked builds the one READONLY_BLOCKED error: permission, exit 5, not retryable, "nothing was sent". Read-only mode is the user's setting, and agents act on next steps, so the steps are only --dry-run (when the CLI's writes have it) and --allow-writes for a write the user authorized. The hint names the switches the CLI lists in ReadOnlyMode (its environment variable and config file) as the user's to change.

terminal.Is asks the operating system whether a stream is a terminal. A file mode check is not enough: /dev/null, which cron, CI and agent runners often give as stdin, is a character device too. A CLI prompts only when terminal.Is(stdin) holds and otherwise returns a structured error naming the flag that answers the prompt (--yes, for example). terminal.Prompter takes the command's streams, so tests drive it without touching the process's stdin and stderr.

Skill read core

skill.Version, VersionsEqual, Loaded, LoadState, Inspect and Installation inspect caller-supplied data and filesystems. Agent, Agents, AgentByID, AgentIDs, Detect and Agent.Destination provide the shared agent integration directory table and destination calculation. The package does not read environment variables, discover home/current directories, print output, construct Cobra commands or write/remove files.

Each CLI owns its embedded filesystem/root/name, environment lookup, output shape and wording, notice timing, and install/uninstall mutation policy. This split preserves the existing unknown version 1 and missing/unreadable installation behavior. Moving the read core does not change Skill content or require a Skill version bump. Consumers need a kit release containing skill before bumping their module pins; a local workspace alone is not a release.

Defaults keep today's behavior

Moving a CLI onto the kit must not change what its users see. Behavior that only some CLIs had is therefore opt-in:

Option Default Who turns it on
transport.Options.SameOriginRedirects off wecom-calendar-cli. The Atlassian CLIs follow cross-origin redirects for attachments and media.
transport.Options.RetryMethods GET and HEAD only wecom-calendar-cli adds PROPFIND and REPORT.
cliflags.InterpretEscapes body flags caller's set bitbucket-cli passes its own body flags.
errors.SetGuidance neutral hints Every CLI installs its own.
auth.Product.ListSchemes off jenkins-cli, prometheus-cli and openobserve-cli name the accepted schemes in AUTH_BAD_SCHEME.
auth.Product.AccountKeyIncludesPath off prometheus-cli, which always keyed secrets by host and path. Turning it on elsewhere would orphan stored secrets.
auth.Scheme hooks (Header, Validate, Decorate) the scheme's Kind decides openobserve-cli for its pass-through token and browser session.
config.URLRules.AssumeScheme a bare host is BAD_BASE_URL jenkins-cli, prometheus-cli and openobserve-cli prefix http://.
config.URLRules.TrimSuffix the path is kept prometheus-cli trims a pasted API root.
config.Paths.Legacy no legacy directory confluence-cli and bitbucket-cli still read their pre-~/.angelmsger directory.
config.Field.ResetBy nothing is reset jira-cli, confluence-cli and bitbucket-cli drop a detected flavor when the server or flavor changes.
config.Schema.AfterMerge none jira-cli, confluence-cli and bitbucket-cli pick basic auth by default on Cloud.
config.Schema.Validate URL, credential page and scheme only jira-cli, confluence-cli and bitbucket-cli check the flavor; prometheus-cli checks the tenant.
config.Schema.CheckPlan none openobserve-cli refuses to repoint a browser-session context.
config.Words.SchemeHint "Use one of: …" wecom-calendar-cli explains its only scheme.

One transport rule is not opt-in: the default Doer follows a redirect only for a read (GET, HEAD, RetryMethods, or a request whose context transport.AsRead marked), and only when the next hop sends the same request. A write gets the redirect itself back, even from a custom Options.Doer that followed it, or that stopped following with an error and returned its last redirect (the error comes back too). A read with a body is held to the same rule: when a custom Doer turned it into a GET on a 301, 302 or 303, the read gets that redirect back, not the GET's page, and is retried only on its own transient failure, never on that page's. A custom Doer keeps *http.Client's contract: a response it returns with an error has its body closed already, and neither the kit nor its caller closes it again. Following it would answer a different request, usually a GET to a login page, or send the write a second time, and a 2xx at the end would report a write that never happened. Under the write rule in errors a write's 3xx is an unknown outcome that keeps its status, when the CLI builds its error from that response. A Doer's error returned with the redirect carries no status, so it is an unknown outcome without one.

A CLI marks a read that cannot be a GET with transport.AsRead(ctx): a search or a query posted because its parameters do not fit in a URL (prometheus-cli's queries, openobserve-cli's search, jira-cli's JQL search). The marked request is retried like a GET and follows a 307 or 308, which sends the same method and body again. A 301, 302 or 303 would turn it into a GET without its body, so a read with a body gets those redirects back too.

The JSON a CLI prints (the error envelope, the list envelope, _notice, the update status, the credential recovery errors, and the configuration errors, set-context plan and auth guide) is pinned by tests. Changing it is a breaking change for every CLI and every agent that reads them.

Credential persistence failures

A successful keychain save removes the same account's older file fallback so an outage cannot bring the old credential back. If that cleanup fails, Save returns the keychain backend together with *credstore.PartialSaveError: a non-empty backend together with an error signals partial persistence. Callers can check those return values without depending on the new error type. They must report that the credential was stored but cleanup is incomplete, and request restored config-directory access before retrying the save.

After a failed keychain write, a file fallback is safe only when the account is explicitly reported missing. If an older value exists or cannot be ruled out, Save returns *credstore.StoreAccessError without writing a fallback. Retry with access to the host user environment and an unlocked keychain. This also means Linux sessions without a reachable D-Bus/Secret Service can now fail to save instead of claiming a login whose credential may later be shadowed. A probe that establishes there is no keychain, or an explicitly missing account, still permits file storage. Existing fallback reads remain available when the keychain cannot be read.

Delete attempts both backends and reports failures instead of claiming logout succeeded. Missing accounts remain a successful, idempotent deletion; other backend failures may leave credentials usable. Callers should preserve that uncertainty and offer a host-access retry. Neither save nor delete errors echo raw keychain diagnostics, which might contain credential material.

Versioning

The kit is built for the family's own CLIs and stays below v1.0.0. A minor release may change the Go API when that makes the kit simpler to use, and the CLIs move to it together; it never changes the JSON a CLI prints. Each CLI pins a released version in its go.mod. For work across the kit and a CLI at once, use a go.work file at the workspace root rather than a replace directive.

Development

gofmt -l .
go vet ./...
go test ./...

CI runs the tests on Linux, macOS and Windows. macOS is the only place the keychain probe runs for real.

License

MIT

Directories

Path Synopsis
Package auth models a CLI's credentials: the schemes it accepts, how a credential goes on the wire, where its secret is stored, and the errors that tell an agent how to recover when the secret cannot be read.
Package auth models a CLI's credentials: the schemes it accepts, how a credential goes on the wire, where its secret is stored, and the errors that tell an agent how to recover when the secret cannot be read.
Package cliflags rewrites argv before cobra parses it, absorbing two common slips an LLM makes when generating a command line:
Package cliflags rewrites argv before cobra parses it, absorbing two common slips an LLM makes when generating a command line:
Package config resolves a CLI's settings from layered sources (flags, the environment, a .env file, a YAML file of named contexts, built-in defaults), records the layer that supplied each value, and reads and writes the file.
Package config resolves a CLI's settings from layered sources (flags, the environment, a .env file, a YAML file of named contexts, built-in defaults), records the layer that supplied each value, and reads and writes the file.
Package credstore persists secrets for a CLI.
Package credstore persists secrets for a CLI.
Package doctor holds the rules every CLI's doctor command shares: how the error a check failed with becomes that check's status, recovery scope and detail in the report.
Package doctor holds the rules every CLI's doctor command shares: how the error a check failed with becomes that check's status, recovery scope and detail in the report.
Package errors defines the structured error model used across the CLI.
Package errors defines the structured error model used across the CLI.
Package output renders command results for either agents or humans.
Package output renders command results for either agents or humans.
Package reuse lets a context adopt a login the user already verified for the same service, so nobody signs in again while a usable credential is stored.
Package reuse lets a context adopt a login the user already verified for the same service, so nobody signs in again while a usable credential is stored.
Package skill provides read-only companion Skill metadata and state checks.
Package skill provides read-only companion Skill metadata and state checks.
Package terminal decides whether a stream is an interactive terminal and asks a person questions on one.
Package terminal decides whether a stream is an interactive terminal and asks a person questions on one.
Package transport provides a flavor-agnostic HTTP layer: a thin client that applies request decorators (auth, user-agent) and retries transient failures.
Package transport provides a flavor-agnostic HTTP layer: a thin client that applies request decorators (auth, user-agent) and retries transient failures.
Package update reports whether a newer release of a CLI is available.
Package update reports whether a newer release of a CLI is available.

Jump to

Keyboard shortcuts

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