Skip to content

Instantly share code, notes, and snippets.

@elithrar
Created January 6, 2026 23:52
Show Gist options
  • Select an option

  • Save elithrar/29c81f84131f68773bc9ba255553b5da to your computer and use it in GitHub Desktop.

Select an option

Save elithrar/29c81f84131f68773bc9ba255553b5da to your computer and use it in GitHub Desktop.
Agent-friendly CLI example using @clack/prompts - outputs YAML schema for machine consumption
#!/usr/bin/env bun
import * as p from "@clack/prompts";
import color from "picocolors";
// ============================================================================
// Types
// ============================================================================
interface Command {
name: string;
description: string;
arguments?: Argument[];
options?: Option[];
usage_examples: string[];
}
interface Argument {
name: string;
description: string;
required: boolean;
}
interface Option {
name: string;
alias?: string;
description: string;
type: "string" | "number" | "boolean";
required?: boolean;
}
interface CLISchema {
name: string;
version: string;
description: string;
commands: Command[];
}
// ============================================================================
// CLI Schema (single source of truth)
// ============================================================================
const CLI_SCHEMA: CLISchema = {
name: "kv-cli",
version: "1.0.0",
description: "A CLI for interacting with the KV store",
commands: [
{
name: "get",
description: "Retrieve a value by key from the KV store",
arguments: [
{
name: "key",
description: "The key to retrieve",
required: true,
},
],
options: [
{
name: "format",
alias: "f",
description: "Output format (json, text)",
type: "string",
},
],
usage_examples: [
"kv-cli get my-key",
"kv-cli get user:123 --format json",
],
},
{
name: "put",
description: "Store a key-value pair in the KV store",
arguments: [
{
name: "key",
description: "The key to store",
required: true,
},
{
name: "value",
description: "The value to store",
required: true,
},
],
options: [
{
name: "ttl",
alias: "t",
description: "Time-to-live in seconds",
type: "number",
},
{
name: "metadata",
alias: "m",
description: "JSON metadata to attach to the key",
type: "string",
},
],
usage_examples: [
'kv-cli put my-key "my value"',
'kv-cli put session:abc "data" --ttl 3600',
'kv-cli put user:123 "data" --metadata \'{"role":"admin"}\'',
],
},
{
name: "list",
description: "List all keys in the KV store",
options: [
{
name: "limit",
alias: "l",
description: "Maximum number of keys to return",
type: "number",
},
{
name: "cursor",
alias: "c",
description: "Pagination cursor for next page",
type: "string",
},
],
usage_examples: ["kv-cli list", "kv-cli list --limit 100"],
},
{
name: "filter",
description: "Filter keys based on criteria",
options: [
{
name: "beforeDate",
alias: "b",
description: "Filter keys created before this date (ISO 8601)",
type: "string",
},
{
name: "afterDate",
alias: "a",
description: "Filter keys created after this date (ISO 8601)",
type: "string",
},
{
name: "prefix",
alias: "p",
description: "Filter keys by prefix",
type: "string",
},
{
name: "limit",
alias: "l",
description: "Maximum number of keys to return",
type: "number",
},
],
usage_examples: [
"kv-cli filter --prefix user:",
"kv-cli filter --afterDate 2024-01-01 --limit 50",
"kv-cli filter --beforeDate 2024-06-01 --afterDate 2024-01-01 --prefix logs:",
],
},
],
};
// ============================================================================
// Stubbed API calls
// ============================================================================
async function kvGet(
key: string,
_options: { format?: string }
): Promise<{ key: string; value: string; metadata?: Record<string, unknown> }> {
// Stub: simulate API latency
await new Promise((r) => setTimeout(r, 200));
return {
key,
value: `<value for ${key}>`,
metadata: { createdAt: new Date().toISOString() },
};
}
async function kvPut(
key: string,
value: string,
_options: { ttl?: number; metadata?: string }
): Promise<{ success: boolean; key: string }> {
await new Promise((r) => setTimeout(r, 200));
return { success: true, key };
}
async function kvList(_options: {
limit?: number;
cursor?: string;
}): Promise<{ keys: string[]; cursor?: string }> {
await new Promise((r) => setTimeout(r, 200));
return {
keys: ["user:123", "user:456", "session:abc", "config:app"],
cursor: undefined,
};
}
async function kvFilter(_options: {
beforeDate?: string;
afterDate?: string;
prefix?: string;
limit?: number;
}): Promise<{ keys: string[]; count: number }> {
await new Promise((r) => setTimeout(r, 200));
return {
keys: ["user:123", "user:456"],
count: 2,
};
}
// ============================================================================
// CLI Output Helpers
// ============================================================================
function printYamlSchema(): void {
const indent = (level: number) => " ".repeat(level);
console.log(`name: ${CLI_SCHEMA.name}`);
console.log(`version: ${CLI_SCHEMA.version}`);
console.log(`description: ${CLI_SCHEMA.description}`);
console.log(`commands:`);
for (const cmd of CLI_SCHEMA.commands) {
console.log(`${indent(1)}- name: ${cmd.name}`);
console.log(`${indent(2)}description: ${cmd.description}`);
if (cmd.arguments?.length) {
console.log(`${indent(2)}arguments:`);
for (const arg of cmd.arguments) {
console.log(`${indent(3)}- name: ${arg.name}`);
console.log(`${indent(4)}description: ${arg.description}`);
console.log(`${indent(4)}required: ${arg.required}`);
}
}
if (cmd.options?.length) {
console.log(`${indent(2)}options:`);
for (const opt of cmd.options) {
console.log(`${indent(3)}- name: --${opt.name}`);
if (opt.alias) console.log(`${indent(4)}alias: -${opt.alias}`);
console.log(`${indent(4)}description: ${opt.description}`);
console.log(`${indent(4)}type: ${opt.type}`);
if (opt.required) console.log(`${indent(4)}required: ${opt.required}`);
}
}
console.log(`${indent(2)}usage_examples:`);
for (const ex of cmd.usage_examples) {
console.log(`${indent(3)}- ${ex}`);
}
}
}
function printHelp(): void {
console.log(`
${color.bold("kv-cli")} - ${CLI_SCHEMA.description}
${color.bold("Usage:")}
kv-cli <command> [options]
${color.bold("Commands:")}
${color.cyan("get")} <key> Retrieve a value by key
${color.cyan("put")} <key> <value> Store a key-value pair
${color.cyan("list")} List all keys
${color.cyan("filter")} Filter keys based on criteria
${color.bold("Options:")}
--help, -h Show help
--version, -v Show version
Run ${color.cyan("kv-cli <command> --help")} for command-specific help.
`);
}
function printCommandHelp(cmd: Command): void {
const args = cmd.arguments?.map((a) => (a.required ? `<${a.name}>` : `[${a.name}]`)).join(" ") ?? "";
console.log(`
${color.bold(cmd.name)} - ${cmd.description}
${color.bold("Usage:")}
kv-cli ${cmd.name} ${args}
`);
if (cmd.arguments?.length) {
console.log(`${color.bold("Arguments:")}`);
for (const arg of cmd.arguments) {
console.log(` ${color.cyan(arg.name)}${arg.required ? " (required)" : ""} ${arg.description}`);
}
console.log();
}
if (cmd.options?.length) {
console.log(`${color.bold("Options:")}`);
for (const opt of cmd.options) {
const alias = opt.alias ? `-${opt.alias}, ` : " ";
console.log(` ${alias}--${opt.name} ${opt.description}`);
}
console.log();
}
console.log(`${color.bold("Examples:")}`);
for (const ex of cmd.usage_examples) {
console.log(` ${color.dim("$")} ${ex}`);
}
console.log();
}
// ============================================================================
// Argument Parsing
// ============================================================================
interface ParsedArgs {
command?: string;
args: string[];
options: Record<string, string | boolean | number>;
}
function parseArgs(argv: string[]): ParsedArgs {
const result: ParsedArgs = { args: [], options: {} };
let i = 0;
while (i < argv.length) {
const arg = argv[i]!;
if (arg.startsWith("--")) {
const key = arg.slice(2);
const next = argv[i + 1];
if (next && !next.startsWith("-")) {
result.options[key] = next;
i += 2;
} else {
result.options[key] = true;
i++;
}
} else if (arg.startsWith("-") && arg.length === 2) {
const alias = arg.slice(1);
const next = argv[i + 1];
// Resolve alias to full name
const fullName = resolveAlias(alias);
if (next && !next.startsWith("-")) {
result.options[fullName] = next;
i += 2;
} else {
result.options[fullName] = true;
i++;
}
} else if (!result.command) {
result.command = arg;
i++;
} else {
result.args.push(arg);
i++;
}
}
return result;
}
function resolveAlias(alias: string): string {
const aliasMap: Record<string, string> = {
h: "help",
v: "version",
f: "format",
t: "ttl",
m: "metadata",
l: "limit",
c: "cursor",
b: "beforeDate",
a: "afterDate",
p: "prefix",
};
return aliasMap[alias] ?? alias;
}
// ============================================================================
// Interactive Mode
// ============================================================================
async function runInteractive(): Promise<void> {
console.clear();
p.intro(color.bgCyan(color.black(" kv-cli ")));
const command = await p.select({
message: "What would you like to do?",
options: CLI_SCHEMA.commands.map((cmd) => ({
value: cmd.name,
label: cmd.name,
hint: cmd.description,
})),
});
if (p.isCancel(command)) {
p.cancel("Operation cancelled.");
process.exit(0);
}
switch (command) {
case "get":
await runInteractiveGet();
break;
case "put":
await runInteractivePut();
break;
case "list":
await runInteractiveList();
break;
case "filter":
await runInteractiveFilter();
break;
}
p.outro(color.green("Done!"));
}
async function runInteractiveGet(): Promise<void> {
const key = await p.text({
message: "Enter the key to retrieve:",
placeholder: "user:123",
validate: (v) => (!v ? "Key is required" : undefined),
});
if (p.isCancel(key)) {
p.cancel("Operation cancelled.");
process.exit(0);
}
const format = await p.select({
message: "Output format:",
options: [
{ value: "json", label: "JSON" },
{ value: "text", label: "Plain text" },
],
});
if (p.isCancel(format)) {
p.cancel("Operation cancelled.");
process.exit(0);
}
const s = p.spinner();
s.start("Fetching value...");
const result = await kvGet(key as string, { format });
s.stop("Value retrieved");
if (format === "json") {
p.note(JSON.stringify(result, null, 2), "Result");
} else {
p.note(result.value, key);
}
}
async function runInteractivePut(): Promise<void> {
const inputs = await p.group({
key: () =>
p.text({
message: "Enter the key:",
placeholder: "user:123",
validate: (v) => (!v ? "Key is required" : undefined),
}),
value: () =>
p.text({
message: "Enter the value:",
placeholder: '{"name": "John"}',
validate: (v) => (!v ? "Value is required" : undefined),
}),
ttl: () =>
p.text({
message: "TTL in seconds (optional):",
placeholder: "3600",
}),
});
if (p.isCancel(inputs)) {
p.cancel("Operation cancelled.");
process.exit(0);
}
const s = p.spinner();
s.start("Storing value...");
const result = await kvPut(inputs.key, inputs.value, {
ttl: inputs.ttl ? parseInt(inputs.ttl, 10) : undefined,
});
s.stop(result.success ? "Value stored" : "Failed to store value");
}
async function runInteractiveList(): Promise<void> {
const limit = await p.text({
message: "Maximum keys to return (optional):",
placeholder: "100",
});
if (p.isCancel(limit)) {
p.cancel("Operation cancelled.");
process.exit(0);
}
const s = p.spinner();
s.start("Listing keys...");
const result = await kvList({
limit: limit ? parseInt(limit, 10) : undefined,
});
s.stop(`Found ${result.keys.length} keys`);
p.note(result.keys.join("\n"), "Keys");
}
async function runInteractiveFilter(): Promise<void> {
const inputs = await p.group({
prefix: () =>
p.text({
message: "Key prefix (optional):",
placeholder: "user:",
}),
afterDate: () =>
p.text({
message: "Created after date (optional):",
placeholder: "2024-01-01",
}),
beforeDate: () =>
p.text({
message: "Created before date (optional):",
placeholder: "2024-12-31",
}),
limit: () =>
p.text({
message: "Maximum keys to return (optional):",
placeholder: "100",
}),
});
if (p.isCancel(inputs)) {
p.cancel("Operation cancelled.");
process.exit(0);
}
const s = p.spinner();
s.start("Filtering keys...");
const result = await kvFilter({
prefix: inputs.prefix || undefined,
afterDate: inputs.afterDate || undefined,
beforeDate: inputs.beforeDate || undefined,
limit: inputs.limit ? parseInt(inputs.limit, 10) : undefined,
});
s.stop(`Found ${result.count} keys`);
p.note(result.keys.join("\n"), "Filtered Keys");
}
// ============================================================================
// Command Execution (non-interactive)
// ============================================================================
async function executeCommand(parsed: ParsedArgs): Promise<void> {
const cmd = CLI_SCHEMA.commands.find((c) => c.name === parsed.command);
if (parsed.options.help && cmd) {
if (process.stdout.isTTY) {
printCommandHelp(cmd);
} else {
printYamlSchema(); // full schema for machine consumption
}
return;
}
switch (parsed.command) {
case "get": {
const key = parsed.args[0];
if (!key) {
console.error("Error: key is required");
process.exit(1);
}
const result = await kvGet(key, { format: parsed.options.format as string });
console.log(JSON.stringify(result, null, 2));
break;
}
case "put": {
const [key, value] = parsed.args;
if (!key || !value) {
console.error("Error: key and value are required");
process.exit(1);
}
const result = await kvPut(key, value, {
ttl: parsed.options.ttl ? Number(parsed.options.ttl) : undefined,
metadata: parsed.options.metadata as string,
});
console.log(JSON.stringify(result, null, 2));
break;
}
case "list": {
const result = await kvList({
limit: parsed.options.limit ? Number(parsed.options.limit) : undefined,
cursor: parsed.options.cursor as string,
});
console.log(JSON.stringify(result, null, 2));
break;
}
case "filter": {
const result = await kvFilter({
beforeDate: parsed.options.beforeDate as string,
afterDate: parsed.options.afterDate as string,
prefix: parsed.options.prefix as string,
limit: parsed.options.limit ? Number(parsed.options.limit) : undefined,
});
console.log(JSON.stringify(result, null, 2));
break;
}
default:
console.error(`Unknown command: ${parsed.command}`);
process.exit(1);
}
}
// ============================================================================
// Main Entry Point
// ============================================================================
async function main(): Promise<void> {
const args = process.argv.slice(2);
const parsed = parseArgs(args);
const isTTY = process.stdout.isTTY;
// --version
if (parsed.options.version) {
console.log(CLI_SCHEMA.version);
return;
}
// No command provided
if (!parsed.command) {
// --help or no args in non-TTY: full YAML schema for machine consumption
if (!isTTY || parsed.options.help) {
if (parsed.options.help && isTTY) {
printHelp();
} else {
printYamlSchema();
}
return;
}
// Interactive mode
await runInteractive();
return;
}
// Execute the specified command (handles command-specific --help)
await executeCommand(parsed);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
{
"name": "agent-friendly-cli",
"module": "index.ts",
"type": "module",
"private": true,
"bin": {
"kv-cli": "./kv-cli.ts"
},
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {
"@clack/core": "^0.5.0",
"@clack/prompts": "^0.11.0",
"picocolors": "^1.1.1"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment