A skill factory for agent-facing CLIs. Install one skill, let your AI agent turn any company API into a CLI and an agent skill — auth, unified output, typed errors and progressive disclosure included. The author writes only "which endpoint to call, how to map fields".
agent-cli-sdk lets you expose a company API to humans and AI agents from a single declaration. It is an SDK (you build a CLI with it), and it is a skill factory: the package ships an agent-cli-builder skill, so an AI agent can generate the whole CLI for you from an API description.
One defineCommand produces three synchronized artifacts:
defineCommand(name / description / zod / run)
│
├── CLI humans + unix pipes (acme orders list | jq)
├── SKILL.md progressive disclosure (generated by `skills gen`)
└── agent dirs synced by `skills sync` (~/.claude, ~/.codex, …)
One source of truth. No drift between the command, the docs, and what the agent reads.
The fastest path — you don't even write the code:
# 1. Add the SDK to a new business package
npm install @renxqoo/agent-cli-sdk# 2. Install the bundled skill into your agent (the package ships it, but does not auto-install it)
"Install this skill for me: https://github.com/renxqoo/agent-cli-sdk/tree/main/skills/agent-cli-builder"
# 3. Give the API to your agent
"Use the agent-cli-builder skill to wrap https://api.acme.com's /orders endpoints
into a CLI named 'acme'. List, get, update orders. OAuth device flow."
The agent creates src/commands/*.ts and src/index.ts, following the SDK contract (Zod schemas, ctx.get, typed errors, errs.*). Then:
# 4. Build, generate the skill, sync it to every agent directory
acme skills gen acme --init # SKILL.md with an auto-generated command table
acme skills sync # ~/.agents + detected ~/.claude/.codex/.cursor/...Done. Teammates run acme orders list; their agents discover acme from the skill description and call it identically. The generated code is buildable and testable, not a sample snippet — the skill enforces the SDK's invariants during generation.
Requires Node.js >= 20. This package is ESM-only (import / dynamic import(); CommonJS require() is not supported).
npm install @renxqoo/agent-cli-sdk
# or
pnpm add @renxqoo/agent-cli-sdkA complete single-command CLI in under 30 lines (no-auth, public data):
#!/usr/bin/env node
import { defineCli, defineCommand } from "@renxqoo/agent-cli-sdk";
import * as z from "zod";
import { realpathSync } from "node:fs";
import { fileURLToPath } from "node:url";
const app = defineCli({
name: "myapp",
description: "My data CLI",
baseUrl: "https://api.example.com",
commands: {
list: defineCommand({
name: "list",
description: "Query list",
args: {
schema: z.object({
limit: z.coerce.number().min(1).max(100).default(20),
}),
},
async run(ctx, args) {
const res = await ctx.get<{ items: Array<{ id: string; title: string }> }>("/items", {
limit: args.limit,
});
return { data: res.data.items, meta: { count: res.data.items.length } };
},
}),
},
});
function isMainEntry(): boolean {
try {
return realpathSync(process.argv[1] ?? "") === fileURLToPath(import.meta.url);
} catch {
return false;
}
}
if (isMainEntry()) app.run(process.argv.slice(2));
export default app;Add OAuth in one line:
import { defineCliApp, defineAuth } from "@renxqoo/agent-cli-sdk";
import { homedir } from "node:os";
import { join } from "node:path";
export default await defineCliApp({
name: "orders",
// One app-owned root; the assembler hands it to every stateful plugin via apply(services).
dir: join(homedir(), ".orders"),
plugins: [
defineAuth({
credentialNamespace: "orders", // → config/orders.json + credentials/orders.json
baseUrl: "https://auth.example.com",
scope: "orders.read offline_access", // business-defined, no default
}),
],
commands: {},
});
// → orders auth login / status / logout / register are auto-injecteddefineCli({
name: 'orders', // required: namespace
description: '...', // required
plugins: [authPlugin], // optional: plugins (auth/logging/audit…)
commands: { list, get }, // required: top-level commands → orders list
namespaces: { orders: {...} }, // optional: sub-namespaces → orders orders list
baseUrl: 'https://api.x.com', // optional: backend address for ctx.get/post/…
errorOnStatus: { 404: 'not_found', '5xx': 'server_error' }, // optional
defaultFormat: 'auto', // optional: 'auto' (default) | 'json' | 'human'
skillsDir: './skills', // optional: enable the built-in skills commands
skillsTargets: [...], // optional: sync targets (omit = default agent dirs)
})import * as z from "zod";
defineCommand({
name: "get",
description: "Query a single order",
args: {
schema: z.object({
id: z.string().min(1).describe("Order ID"),
verbose: z.boolean().describe("Verbose output").default(false),
}),
pos: ["id"], // `id` is a positional operand, not a same-name flag
},
humanFormat: (data) => `Order: ${data.id}`, // optional: custom --no-json text
async run(ctx, args) {
const res = await ctx.get(`/orders/${args.id}`); // ctx.get/post/put/patch/delete
return { data: res.data };
},
});args is optional (omit = no business parameters). Its Zod object is the only validation and type source. args.type defaults to "argv"; set it to "json" for a single complete document via --input / --input-file / stdin (see docs/07-structured-input.md).
const auth = defineAuth({
credentialNamespace: "crm", // → config/crm.json + credentials/crm.json
baseUrl: AUTH_BASE_URL, // OAuth middleware
scope: "company.api offline_access", // one scope for login + registration
// flow: 'device', // default 'device' | 'authorization_code' | 'client_credentials'
// commandNamespace: 'auth', // default 'auth' → crm auth login
});Three OAuth 2.1 flows, one factory: device (RFC 8628, default), authorization_code + PKCE, and client_credentials. It returns a Plugin — drop it into defineCliApp({ plugins: [auth] }) and the login/status/logout/register commands are auto-mounted.
const myPlugin = {
name: "audit",
enforce: "pre", // 'pre' | 'post' (default normal)
provides: {
commands: { telemetry: telemetryCmd }, // contribute commands
namespaces: { admin: { users: userCmd } },
},
async beforeRequest(ctx, req) {
return { ...req, headers: { ...req.headers, "x-client": "my-cli" } };
},
async transformOutput(ctx, data) {
return data;
},
async handleUnauthorized(ctx, event) {
return { action: "decline" };
},
};Commands contributed via a plugin's provides are automatically exempted from that plugin's own beforeCommand, but not from other plugins. See docs/02-sdk-guide.md.
Success (stdout):
{"ok":true,"source":"orders","data":{"orders":[...]},"meta":{"count":2,"pagination":{"complete":true}}}Error (stderr):
{
"ok": false,
"error": {
"type": "api",
"subtype": "not_found",
"message": "Order not found",
"hint": "Check the ID"
}
}Exit codes (set automatically by error category; agents branch on them):
| code | category | meaning |
|---|---|---|
| 0 | — | success |
| 1 | api | server-side business error (404/500/429…) |
| 2 | validation | invalid parameter |
| 3 | authentication / authorization / config | login required / missing permission / missing config |
| 4 | network | DNS / timeout / connection refused |
| 5 | internal | SDK internal error (should rarely happen) |
| 6 | policy | risk-control block |
| 10 | confirmation | high-risk write requires --yes |
Nine typed error classes — ValidationError / AuthenticationError / PermissionError / ConfigError / NetworkError / APIError (with NotFoundError) / PolicyError / InternalError / ConfirmationRequiredError. Always throw errs.*; a bare Error is downgraded to internal/unknown.
Output modes: auto (TTY → text, pipe/script → JSON) by default, overridable with --json / --no-json or defaultFormat. Agents and scripts should always pass --json.
<bin> skills gen <name> --init— generate aSKILL.mdskeleton with an auto-generated command table (AUTO-GEN region).<bin> skills gen <name>— refresh only the auto-generated block; hand-written semantics are preserved.<bin> skills sync— copy skills to installed agent dirs (~/.agentsalways;~/.claude/~/.codex/~/.cursor/~/.zcode/~/.openclaw/~/.piwhen present).<bin> skills list/<bin> skills read <name>— list / read bundled skills.
Agents load skills lazily: they start with only name + description, expand the full SKILL.md when a task matches, and read references/ only on demand — so unused APIs cost no tokens.
Design docs ship in docs/ — English as *.en.md, Chinese as *.md:
| Doc | Content |
|---|---|
00-overview.en.md |
Architecture, layering, decision checklist |
01-cli-usage.en.md |
Command invocation, pipes, pagination, exit codes |
02-sdk-guide.en.md |
SDK usage, ctx interface, hooks |
03-envelopes.en.md |
Unified output field contract |
04-errors.en.md |
9 error classes, when to throw |
05-credentials.en.md |
Provider chain, custom credentials |
06-skills.en.md |
Skill system, command doc auto-generation |
07-structured-input.md |
Structured payloads, validation, write policies (English; see 07-structured-input.zh-CN.md for Chinese) |
The npm package ships the agent-cli-builder skill — the agent-facing guide to building a CLI with this SDK — plus 11 references (core API, auth patterns, error catalog, plugin patterns, skill generation, testing, …).
pnpm install
pnpm build # tsup → dist/
pnpm typecheck # tsc (incl. type-tests)
pnpm test # vitest
pnpm test:package # npm tarball smoke testSee the contribution guide, security policy, and support policy before opening a pull request or report.