agent-cli-kit

module
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Nov 4, 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) 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).
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.

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 and RetryMethods). A write gets the redirect itself back. 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.

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