An embeddable Go core for running authenticated CLI/API tools with injected credentials.
AnyCLI is a library, not a standalone CLI. A host program embeds it in-process, supplies a CredentialResolver for its own credential sources, and calls Execute. AnyCLI loads the matching embedded tool definition, resolves and injects credentials (env var / CLI flag / ephemeral config file), runs before/after middleware, and execs the underlying binary or built-in service.
The supported tools and their definitions live inside AnyCLI (embedded JSON). The embedder never supplies tool definitions — only a credential resolver and an optional cache.
LLMs are trained on billions of CLI examples. They already know git, gh, curl, jq, and thousands of other tools. MCP forces agents to learn new schemas from scratch, consuming far more tokens per operation with lower reliability. CLI is the natural interface between agents and the world. Read the full rationale.
AnyCLI's job is the missing piece: let an agent run those tools authenticated, without the host shipping long-lived secrets to disk. The host owns where credentials come from; AnyCLI owns how they are injected and how long they live.
import "github.com/heliohq/anycli"
// 1. Construct an engine. Cache is optional; nil uses an in-memory default.
engine, err := anycli.New(anycli.Config{
Cache: myCache, // optional: implements anycli.Cache
})
if err != nil {
return err
}
// 2. Run a tool. The resolver supplies credentials for this tool; AnyCLI
// loads the embedded definition, injects, and execs.
exitCode, err := engine.Execute(ctx, anycli.Tool("gh"), []string{"pr", "list"}, resolver)type Config struct {
// Cache is the credential cache the engine uses to avoid re-resolving on
// every call. Optional — nil installs an in-memory cache. The on-disk
// cache that the old standalone CLI used is gone; the host decides the
// storage (per-process, per-assistant, shared, …) by supplying a Cache.
Cache Cache
}The resolver is the only thing that crosses the boundary into AnyCLI. It returns in-memory data; AnyCLI owns injection, caching, and lifecycle. Where the data comes from (stored OAuth, an on-demand minted token, anything) is entirely the host's business — the resolver never learns how the data is injected.
type CredentialResolver interface {
// Resolve returns the credential fields for one tool/account pair, plus
// when they go stale. An empty account selects the host's default.
Resolve(ctx context.Context, tool Tool, account string) (*Credential, error)
}
type Credential struct {
Data map[string]string // credential fields; bindings index into it by field name
CacheUntil time.Time // when this credential goes stale (drives the cache)
}type Cache interface {
Get(key string) (*CacheEntry, bool)
Set(key string, entry *CacheEntry)
MarkStale(key string)
}CacheEntry stores only the extracted credential fields plus CacheUntil/Stale. The engine derives a collision-free key from (tool, account). Supply your own cache or pass nil to use the built-in in-memory implementation.
ListTools returns credential-safe manifests for every embedded tool in deterministic name order. Discovery also validates that service definitions have registered implementations and CLI definitions declare a binary.
tools, err := anycli.ListTools()Tool is type Tool string; pass a raw anycli.Tool("…") whose name matches
an embedded definition. The built-in service tools are slack, notion,
gmail, discord, linkedin, x, figma, mongodb, and supabase;
github wraps the gh binary. An unknown tool is an error from Execute, not
a compile error.
The Figma service uses personal access tokens and exposes every PAT-compatible
operation in the pinned official OpenAPI catalog: 47 operations across files,
projects, comments/reactions, libraries, variables, Dev Mode resources,
webhooks, library analytics, payments, and oEmbed. Every operation has a named
command and is also callable by operation ID through figma api call.
Agent-oriented commands accept Figma URLs directly: figma context metadata,
context design, context figjam, context screenshot, and context variables. figma assets download and download-fills materialize signed
render/image-fill URLs without forwarding the PAT to the asset host. Run
figma capabilities for the machine-readable boundary: PAT REST has no general
native-canvas create/edit/delete API, so hosted-MCP operations such as
use_figma, new-file/design generation, uploads, shaders, and native Code
Connect mappings require Figma's hosted MCP or a Figma plugin bridge.
The gmail service projects the Gmail API v1 users.* resource namespaces
(profile, messages, messages attachments, threads, drafts, labels)
plus the synthetic reply / forward verbs, which assemble the threading
headers (In-Reply-To / References / threadId) and the quoted original
inside the tool. --query passes native Gmail search syntax through verbatim;
messages modify batches multiple ids via batchModify; send/reply support
--body/--body-file, --html, and repeated --attach (multipart MIME up
to 25MB). Every command supports --json; list commands page with --max and
--page-token. It replaces the retired google tool (per-app split).
The x service supports OAuth 2.0 user-context identity and user lookup,
recent post search, timelines, post/reply/thread/repost management, simple
JPEG/PNG/WebP uploads for posts or DMs, alt text, and legacy Direct Messages.
Every list/search command retrieves one explicit page; callers pass the
returned token with --next-token. XChat and chunked video/GIF uploads are not
part of this surface.
The mongodb service is a thin wrapper around the official MongoDB Shell
(wraps mongosh 2.9.2) connecting with a standard connection string
(mongodb:// or mongodb+srv://) resolved into MONGODB_CONNECTION_STRING.
It exposes exactly two commands: eval '<mongosh JS>' and ping. Database
selection happens in the script (db.getSiblingDB(...)); db is
pre-connected via a connect(process.env.MONGODB_CONNECTION_STRING) prelude
(which then deletes the variable from process.env), so the DSN never appears
on the command line. Queries default to secondaryPreferred; callers use
--read-preference primary when a script requires immediate read-after-write
consistency. The default timeout is two minutes and can be changed with
--timeout. Direct db.runCommand() calls manage their own read preference.
mongosh flags are fixed
(--nodb --quiet --norc --json=relaxed) and not passed through — the script
travels as a fused --eval= token, so --shell and other flags are
unreachable. Output is relaxed extended JSON. The first invocation lazily
installs mongosh from downloads.mongodb.com (sha256-verified, file-locked,
bounded by its own install timeout) into the pinned-versions directory unless
a mongosh is already on PATH, in which case the PATH mongosh is used as-is —
but only while no pinned install exists yet; once the pin lands it wins over
PATH. Lazy install extracts the mongosh binary only, not the
mongosh_crypt_v1 shared library, so automatic client-side field-level
encryption (CSFLE) is unavailable through a lazily-installed mongosh (a PATH
mongosh from a full distribution keeps it). In --json mode mongosh reports a
thrown error as a JSON error object on stdout; a codeName of
AuthenticationFailed (or auth-failure message text) rejects the credential,
while permission errors (Unauthorized / "not authorized") and transport
failures do not. Output redacts the connection string and its password as a
guard against accidental echo (not deliberate exfiltration by the script).
The supabase service wraps the official Supabase CLI 2.115.0 with a fixed,
inspectable command tree and injects the resolved OAuth token as
SUPABASE_ACCESS_TOKEN. It covers token-authenticated organization/project
discovery, preview branches, Edge Functions, selected project and database
configuration, SSO reads, SQL snippets, schema type generation, Storage copy
and listing, and db query. AnyCLI selects the allowed command paths and
classifies their side effects, then forwards each allowed command's arguments
and flags verbatim to the official CLI. Database queries and storage cp are
always classified as side effects for host approval. The service intentionally
excludes login, local stack lifecycle, migration commands, arbitrary command
paths, secret-value reads/writes, and Storage deletion. Experimental,
confirmation, and Function Docker/API choices remain caller-controlled through
official flags such as --experimental, --yes, and --use-api. The first
invocation lazily installs the pinned official archive with sha256 verification;
commands append JSON/agent output controls, use an isolated CLI home, disable
telemetry, and enforce a fixed 15-minute execution limit.
cmd/anycli builds a standalone binary for tool-definition development. It is
NOT the product (the library is); it exists so a definition can be exercised
against the real provider API before any host integration:
make build-harness
ANYCLI_CRED_ACCESS_TOKEN=xoxb-... ./bin/anycli slack -- chat send --channel C1 --text hi
./bin/anycli list
Credentials come from ANYCLI_CRED_<FIELD> environment variables (suffix
lowercased = credential field name). Everything after -- goes to the tool
verbatim.
- Why AnyCLI — CLI vs MCP for agents
- Credential Lifecycle — how credentials are resolved, cached, and injected at runtime
- Tool Definition Schema — field reference for the embedded tool definitions
- Side Effects — the
anycli.side_effectannotation, howInspectreports it, and how a host consumes it - E2E Tests — real-provider test conventions, local credentials, and nightly execution
- Design 001: Vault Credential Integration — superseded historical vault/local design
- Design 002: Embeddable Core + Credential Resolver — the library architecture
- Design 003: Multi-account + Helio toolset — account-aware execution and built-in services
- Design 005: Tool Manifest Discovery — deterministic discovery and definition/executor validation
- Design 006: Figma REST Service — PAT-authenticated Figma command surface
Apache License 2.0