skills/steipete-cli-builder/SKILL.md
Build agent-grade CLI tools using Peter Steinberger's proven patterns. Covers CLI-first architecture, MCP integration, crash-resilient wrappers, multi-provider AI, session management, and distribution. Use when creating CLI tools, MCP servers, or AI-integrated automation tools.
npx skillsauth add szoloth/skills steipete-cli-builderInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Build production-grade CLI tools optimized for AI agent integration using patterns distilled from Peter Steinberger's 30+ tool portfolio (Peekaboo, MCPorter, Oracle, Tachikoma, Terminator).
The CLI is the canonical engine. MCP/API wrappers are thin adapters.
┌─────────────────────────────────────────────────────────────┐
│ Distribution Layer │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Homebrew │ │ npm/npx │ │ GitHub Releases │ │
│ │ (humans) │ │ (agents) │ │ (binaries) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
└─────────┼────────────────┼────────────────────┼─────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ MCP Wrapper Layer │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Node.js supervisor (crash recovery, signal forwarding) │ │
│ └────────────────────────────┬────────────────────────────┘ │
└───────────────────────────────┼─────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ Native CLI Core │
│ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ Commands │ │ Services │ │ AI Provider │ │
│ │ (Parser) │→→│ (Locator) │→→│ (Tachikoma) │ │
│ └──────────────┘ └──────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Why CLI-first?
| Platform | Language | CLI Parser | When to Use | |----------|----------|------------|-------------| | macOS-native | Swift 6 | Commander, ArgumentParser | UI automation, ScreenCaptureKit, Accessibility APIs | | Cross-platform | TypeScript | Commander.js v14+ | Most CLI tools, MCP servers | | Multi-service API | Go | Kong | API clients, OAuth flows, static binaries |
mytool/
├── src/
│ ├── cli.ts # CLI entry point (Commander.js)
│ ├── index.ts # Library exports
│ ├── commands/
│ │ ├── process.ts # Subcommand handlers
│ │ └── list.ts
│ ├── lib/
│ │ ├── errors.ts # CLIError class
│ │ ├── logger.ts # Verbose logging
│ │ ├── formatter.ts # Output formatting
│ │ ├── processor.ts # Core logic with fallbacks
│ │ └── providers/ # AI provider abstraction
│ │ ├── index.ts
│ │ ├── openai.ts
│ │ └── anthropic.ts
│ ├── mcp/
│ │ └── server.ts # MCP server mode
│ └── types.ts
├── knowledge_base/ # Embedded prompts/templates
│ ├── prompts/
│ └── templates/
├── bin/
│ ├── mytool # CLI wrapper
│ └── mytool-mcp.js # MCP supervisor
├── package.json
├── tsconfig.json
├── AGENTS.md # AI agent conventions
└── README.md
{
"name": "@myorg/mytool",
"version": "1.0.0",
"type": "module",
"bin": {
"mytool": "./dist/cli.js",
"mytool-mcp": "./bin/mytool-mcp.js"
},
"exports": {
".": "./dist/index.js"
},
"files": ["dist", "bin", "knowledge_base"],
"scripts": {
"build": "tsc",
"dev": "tsx src/cli.ts",
"mcp:serve": "node bin/mytool-mcp.js",
"test": "vitest",
"prepublishOnly": "npm run build"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.0",
"commander": "^14.0.0",
"zod": "^4.0.0",
"zod-to-json-schema": "^3.23.0",
"fuse.js": "^7.0.0"
},
"engines": {
"node": ">=22"
},
"postinstall": "chmod +x bin/* 2>/dev/null || true"
}
Every command must support these flags:
import { Command } from 'commander';
const program = new Command();
program
.name('mytool')
.version(loadVersion())
.description('Tool description')
.option('-v, --verbose', 'Enable debug logging', false)
.option('--json', 'Output as JSON', false)
.option('--dry-run', 'Preview without executing', false)
.option('-q, --quiet', 'Minimal output', false)
.hook('preAction', (cmd) => {
const opts = cmd.opts();
setVerbose(opts.verbose);
});
// Subcommand with same flags inherited
program
.command('process <input>')
.description('Process input file or URL')
.option('-m, --model <model>', 'AI model to use', 'gpt-4')
.action(async (input, options) => {
const globalOpts = program.opts();
try {
const result = await processInput(input, options);
output(result, globalOpts);
} catch (err) {
handleError(err, globalOpts.json);
}
});
program.parse();
// lib/errors.ts
export class CLIError extends Error {
constructor(
message: string,
public exitCode: number = 1,
public details?: Record<string, unknown>
) {
super(message);
this.name = 'CLIError';
}
}
// Standard exit codes
export const EXIT_CODES = {
SUCCESS: 0,
GENERAL_ERROR: 1,
INVALID_ARGS: 2,
FILE_NOT_FOUND: 3,
PERMISSION_DENIED: 4,
NETWORK_ERROR: 5,
INTERRUPTED: 130
} as const;
export function handleError(err: unknown, jsonMode: boolean): never {
const message = err instanceof Error ? err.message : String(err);
const code = err instanceof CLIError ? err.exitCode : 1;
const details = err instanceof CLIError ? err.details : undefined;
if (jsonMode) {
console.error(JSON.stringify({ error: message, code, ...details }));
} else {
console.error(`Error: ${message}`);
}
process.exit(code);
}
The MCP wrapper is a crash-resilient supervisor that spawns the CLI.
#!/usr/bin/env node
// bin/mytool-mcp.js
import { spawn, execFileSync } from 'child_process';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { existsSync } from 'fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const BINARY = join(__dirname, '..', 'dist', 'cli.js');
const MAX_RESTARTS = 5;
const RESTART_WINDOW_MS = 60_000;
const INITIAL_DELAY_MS = 1000;
const MAX_DELAY_MS = 30_000;
class MCPWrapper {
constructor() {
this.restartTimestamps = [];
this.delay = INITIAL_DELAY_MS;
this.child = null;
this.shuttingDown = false;
}
start() {
// Rate limit restarts
const now = Date.now();
this.restartTimestamps = this.restartTimestamps.filter(
ts => now - ts < RESTART_WINDOW_MS
);
if (this.restartTimestamps.length >= MAX_RESTARTS) {
console.error(`[MCP] Aborting: ${MAX_RESTARTS} restarts in 60s`);
process.exit(1);
}
console.error('[MCP] Starting server...');
this.child = spawn('node', [BINARY, 'mcp', 'serve'], {
stdio: 'inherit',
env: { ...process.env, MCP_WRAPPER: 'true' }
});
this.child.on('exit', (code, signal) => {
if (this.shuttingDown) return process.exit(code || 0);
if (code === 0 || signal === 'SIGINT' || signal === 'SIGTERM') {
process.exit(code || 0);
}
this.handleCrash(code, signal);
});
this.child.on('error', (err) => {
console.error('[MCP] Launch failed:', err.message);
this.handleCrash(1);
});
}
handleCrash(code, signal) {
console.error(`[MCP] Crashed (code ${code})`);
this.restartTimestamps.push(Date.now());
setTimeout(() => {
this.delay = Math.min(this.delay * 2, MAX_DELAY_MS);
this.start();
}, this.delay);
}
shutdown() {
this.shuttingDown = true;
if (this.child && !this.child.killed) {
this.child.kill('SIGTERM');
}
}
}
const wrapper = new MCPWrapper();
wrapper.start();
process.on('SIGINT', () => wrapper.shutdown());
process.on('SIGTERM', () => wrapper.shutdown());
// src/mcp/server.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
const server = new Server({
name: 'mytool',
version: '1.0.0',
capabilities: { tools: {} }
});
// Define schemas (reuse from CLI)
const ProcessSchema = z.object({
input: z.string().describe('Input file or URL'),
model: z.string().optional().default('gpt-4'),
format: z.enum(['text', 'json', 'markdown']).optional().default('text')
});
// Register tools
server.setRequestHandler('tools/list', async () => ({
tools: [{
name: 'process',
description: 'Process input with AI analysis',
inputSchema: zodToJsonSchema(ProcessSchema)
}]
}));
server.setRequestHandler('tools/call', async (request) => {
const { name, arguments: args } = request.params;
if (name === 'process') {
const input = ProcessSchema.parse(args);
const result = await processInput(input);
return { content: [{ type: 'text', text: result.formatted }] };
}
throw new Error(`Unknown tool: ${name}`);
});
// Start server
const transport = new StdioServerTransport();
await server.connect(transport);
// src/lib/providers/index.ts
export interface AIProvider {
name: string;
generate(prompt: string, options?: GenerateOptions): Promise<string>;
stream(prompt: string, options?: GenerateOptions): AsyncIterable<string>;
}
export interface GenerateOptions {
model?: string;
maxTokens?: number;
temperature?: number;
}
// Credential resolution chain
export function resolveApiKey(provider: string): string {
// 1. Environment variable
const envKey = process.env[`${provider.toUpperCase()}_API_KEY`];
if (envKey) return envKey;
// 2. Credential file
const credPath = join(homedir(), '.mytool', 'credentials');
if (existsSync(credPath)) {
const creds = JSON.parse(readFileSync(credPath, 'utf-8'));
if (creds[provider]) return creds[provider];
}
throw new CLIError(
`No API key for ${provider}. Set ${provider.toUpperCase()}_API_KEY`,
1
);
}
// Factory
export function createProvider(name: string): AIProvider {
switch (name) {
case 'openai': return new OpenAIProvider();
case 'anthropic': return new AnthropicProvider();
case 'google': return new GoogleProvider();
default: throw new CLIError(`Unknown provider: ${name}`);
}
}
// src/lib/processor.ts
interface Strategy<T> {
name: string;
fn: () => Promise<T>;
}
export async function withFallback<T>(
strategies: Strategy<T>[],
verbose = false
): Promise<T> {
let lastError: Error | null = null;
for (const strategy of strategies) {
try {
if (verbose) console.error(`[DEBUG] Trying ${strategy.name}...`);
return await strategy.fn();
} catch (err) {
lastError = err as Error;
if (verbose) console.error(`[DEBUG] ${strategy.name} failed: ${err.message}`);
}
}
throw new CLIError(
`All strategies failed: ${lastError?.message}`,
1
);
}
// Usage
const content = await withFallback([
{ name: 'api', fn: () => fetchViaAPI(url) },
{ name: 'scrape', fn: () => fetchViaScrape(url) },
{ name: 'cache', fn: () => fetchFromCache(url) }
], options.verbose);
// src/lib/knowledge-base.ts
import Fuse from 'fuse.js';
import { readdirSync, readFileSync } from 'fs';
import { join, basename } from 'path';
interface KnowledgeEntry {
id: string;
title: string;
content: string;
keywords: string[];
}
export class KnowledgeBase {
private entries: KnowledgeEntry[] = [];
private fuse: Fuse<KnowledgeEntry>;
constructor(basePath: string) {
this.loadEntries(basePath);
this.fuse = new Fuse(this.entries, {
keys: ['title', 'keywords', 'content'],
threshold: 0.4,
includeScore: true
});
}
private loadEntries(dir: string) {
const files = readdirSync(dir, { recursive: true });
for (const file of files) {
if (file.endsWith('.md')) {
const content = readFileSync(join(dir, file), 'utf-8');
const title = basename(file, '.md');
this.entries.push({
id: file,
title,
content,
keywords: this.extractKeywords(content)
});
}
}
}
search(query: string, limit = 5): KnowledgeEntry[] {
return this.fuse.search(query, { limit }).map(r => r.item);
}
get(id: string): KnowledgeEntry | undefined {
return this.entries.find(e => e.id === id);
}
}
learn command)// src/commands/learn.ts
program
.command('learn')
.description('Output comprehensive guide for AI agents')
.action(async () => {
const opts = program.opts();
const guide = {
systemPrompt: `You are using ${pkg.name} v${pkg.version}.
This tool provides the following capabilities...`,
tools: allTools.map(tool => ({
name: tool.name,
description: tool.description,
parameters: zodToJsonSchema(tool.schema),
examples: tool.examples
})),
capabilities: [
'Process files and URLs with AI analysis',
'Search embedded knowledge base',
'Multi-provider AI integration'
],
constraints: [
'Requires API key for AI operations',
'Maximum input size: 100MB'
],
quickStart: [
`${pkg.name} process <input> --model gpt-4`,
`${pkg.name} search "query" --limit 5`,
`${pkg.name} --json process <input>`
]
};
if (opts.json) {
console.log(JSON.stringify(guide, null, 2));
} else {
console.log(renderGuideAsMarkdown(guide));
}
});
// src/lib/sessions.ts
interface Session {
id: string;
startedAt: Date;
lastActivity: Date;
task: string;
state: 'running' | 'paused' | 'completed';
context: Record<string, unknown>;
}
export class SessionStore {
private dir: string;
constructor(baseDir = '~/.mytool/sessions') {
this.dir = expandHome(baseDir);
mkdirSync(this.dir, { recursive: true });
}
async create(task: string): Promise<Session> {
const session: Session = {
id: crypto.randomUUID(),
startedAt: new Date(),
lastActivity: new Date(),
task,
state: 'running',
context: {}
};
await this.save(session);
return session;
}
async list(options?: { hours?: number }): Promise<Session[]> {
const files = readdirSync(this.dir);
const sessions = await Promise.all(
files.map(f => this.get(f.replace('.json', '')))
);
if (options?.hours) {
const cutoff = Date.now() - options.hours * 60 * 60 * 1000;
return sessions.filter(s => s && s.startedAt.getTime() > cutoff);
}
return sessions.filter(Boolean);
}
async mostRecent(): Promise<Session | null> {
const sessions = await this.list();
return sessions.sort((a, b) =>
b.lastActivity.getTime() - a.lastActivity.getTime()
)[0] ?? null;
}
}
# Formula/mytool.rb
class Mytool < Formula
desc "Description of my tool"
homepage "https://github.com/myorg/mytool"
version "1.0.0"
license "MIT"
on_macos do
if Hardware::CPU.arm?
url "https://github.com/myorg/mytool/releases/download/v#{version}/mytool-darwin-arm64.tar.gz"
sha256 "abc123..."
else
url "https://github.com/myorg/mytool/releases/download/v#{version}/mytool-darwin-amd64.tar.gz"
sha256 "def456..."
end
end
def install
bin.install "mytool"
end
def caveats
<<~EOS
Set your API key:
export OPENAI_API_KEY=your-key-here
EOS
end
test do
assert_match version.to_s, shell_output("#{bin}/mytool --version")
end
end
--json for machine-readable outputlearn command generates comprehensive docs in one call# Repository Guidelines
## Build Commands
- `npm install` - Install dependencies
- `npm run build` - Build TypeScript
- `npm run dev` - Run in development mode
- `npm run mcp:serve` - Start MCP server
- `npm test` - Run tests
## CLI Conventions
- All commands support `--json` for machine output
- All commands support `-v, --verbose` for debug logging
- All commands support `--dry-run` for preview
- Exit codes follow standard conventions (0=success, 1=error)
- Errors go to stderr, output goes to stdout
## MCP Development
- No console.log in MCP tools (breaks stdio protocol)
- All tools must have Zod schemas
- Test with `npx @anthropic-ai/mcp-inspector`
## Git Conventions
- Conventional commits: `feat(cli): add process command`
- No force pushes to main
content-media
Fetch transcripts from YouTube videos for summarization and analysis.
documentation
This skill should be used when reviewing or editing written drafts to ensure they match Sam's personal style guide. It prioritizes voice preservation and anti-beige detection while catching structural gaps. Triggers on requests to review, edit, or improve written content.
tools
Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.
development
Web search and content extraction using Brave Search. Use when researching topics, finding documentation, extracting article content, or gathering information from the web. No browser required - works headlessly.