Creating Tools
If you can write a CLI script that outputs JSON, you can create a cli4ai tool that works with AI agents (and can also be run directly).
1) Scaffold a Tool
Create a new tool project with cli4ai init:
cli4ai init my-tool
cd my-tool Templates:
# Basic tool (default)
cli4ai init my-tool
# API wrapper (includes .env.example)
cli4ai init my-api-tool --template api
# Browser automation (puppeteer)
cli4ai init my-browser-tool --template browser Optional runtimes:
# Node (ESM)
cli4ai init my-node-tool --runtime node
# Deno (no npm deps)
cli4ai init my-deno-tool --runtime deno This creates a basic structure (bun template shown):
my-tool/
├── cli4ai.json # Package manifest (source of truth)
├── run.ts # Entry point
├── package.json # Dev/publish metadata (and dependency install target)
├── README.md
└── .gitignore
The API template also creates .env.example. The node runtime uses run.mjs instead of run.ts.
2) Define the Manifest (cli4ai.json)
cli4ai.json is the source of truth for cli4ai:
it describes commands (for MCP + documentation), required secrets, dependencies to install, and runtime.
{
"name": "my-tool",
"version": "1.0.0",
"description": "A description of what your tool does",
"author": "your-name",
"license": "MIT",
"entry": "run.ts",
"runtime": "bun",
"keywords": ["productivity", "automation"],
"commands": {
"hello": {
"description": "Say hello",
"args": [
{ "name": "name", "required": false, "description": "Name to greet" }
]
},
"fetch": {
"description": "Fetch JSON from an API endpoint",
"args": [
{ "name": "endpoint", "required": true, "description": "API endpoint path (e.g. users/123)" }
],
"options": [
{ "name": "raw", "type": "boolean", "description": "Return raw API JSON" }
]
}
},
"env": {
"API_KEY": {
"required": true,
"description": "Your API key (stored via cli4ai secrets)",
"secret": true
},
"DEBUG": {
"required": false,
"description": "Enable debug mode",
"default": "false"
}
},
"dependencies": {
"@cli4ai/lib": "^1.0.0",
"commander": "^14.0.0"
},
"peerDependencies": {
"gh": ">=2.0.0"
},
"mcp": {
"enabled": true,
"transport": "stdio"
}
} Manifest Fields
| Field | Required | Description |
|---|---|---|
name | Yes | Package name (lowercase, hyphens) |
version | Yes | Semantic version (x.y.z) |
entry | Yes | Entry point file (e.g., run.ts) |
commands | No* | Recommended; used for cli4ai info, MCP tool descriptions, and config generation |
runtime | No | bun (default), node, or deno |
env | No | Environment variables |
dependencies | No | npm packages to install |
peerDependencies | No | System tools required (gh, ffmpeg, etc.) |
mcp | No | Enable MCP server mode (enabled, transport) |
* Tools can run without commands, but you should define them if you want great MCP/agent integration.
The Entry Point (run.ts)
Your entry point is a single CLI script. For consistent JSON output, error handling, and env loading,
use the cli4ai SDK: @cli4ai/lib.
#!/usr/bin/env bun
import { cli, env, output, outputError, withErrorHandling } from '@cli4ai/lib/cli.ts';
const program = cli('my-tool', '1.0.0', 'My awesome tool');
// Define commands
program
.command('hello [name]')
.description('Say hello')
.action((name?: string) => output({ message: `Hello, ${name?.trim() || 'World'}!` }));
program
.command('fetch ')
.description('Fetch JSON from an API endpoint')
.option('--raw', 'Return raw API JSON')
.action(withErrorHandling(async (endpoint: string, options: { raw?: boolean }) => {
// If you run this via cli4ai, missing required secrets will be prompted for automatically.
const apiKey = env('API_KEY');
const response = await fetch(`https://api.example.com/${endpoint}`, {
headers: { 'Authorization': `Bearer ${apiKey}` }
});
if (!response.ok) {
outputError('API_ERROR', `HTTP ${response.status}: ${response.statusText}`);
}
const json = await response.json();
output(options.raw ? json : { endpoint, data: json });
}));
program.parse(); 3) Best Practices
1. Always Output JSON
AI agents need parseable output. Prefer output() over printing text:
// Good
output({ users: [...], count: 10 });
// Also good (errors should be JSON too)
outputError('NOT_FOUND', 'User not found', { userId: '123' });
// Bad (hard to parse reliably)
console.log("Found 10 users:");
users.forEach(u => console.log(`- ${u.name}`)); 2. Use Standard Error Codes
Consistent error handling helps AI agents understand failures. Use outputError(code, message):
const ErrorCodes = {
ENV_MISSING: 'ENV_MISSING', // Missing environment variable
INVALID_INPUT: 'INVALID_INPUT', // Bad user input
NOT_FOUND: 'NOT_FOUND', // Resource not found
AUTH_FAILED: 'AUTH_FAILED', // Authentication failed
API_ERROR: 'API_ERROR', // External API error
NETWORK_ERROR: 'NETWORK_ERROR', // Network failure
RATE_LIMITED: 'RATE_LIMITED', // Rate limit hit
TIMEOUT: 'TIMEOUT', // Operation timed out
PARSE_ERROR: 'PARSE_ERROR', // Invalid JSON, etc.
}; 3. Define All Commands in Manifest
Commands in cli4ai.json become MCP tools and show up in cli4ai info:
{
"commands": {
"list": {
"description": "List all items",
"args": [
{ "name": "limit", "required": false, "description": "Max results" }
]
},
"get": {
"description": "Get a specific item",
"args": [
{ "name": "id", "required": true, "description": "Item ID" }
]
},
"create": {
"description": "Create a new item",
"args": [
{ "name": "name", "required": true },
{ "name": "description", "required": false }
]
}
}
}
Note: commands is metadata for agents/MCP; your entry script still defines the actual CLI behavior. Keep them in sync.
4. Mark Secrets Appropriately
Declare required secrets in the manifest. When you run via cli4ai, missing required secrets will prompt once and be saved:
{
"env": {
"API_KEY": {
"required": true,
"secret": true,
"description": "API key (stored securely)"
}
}
} # Set manually (recommended for automation)
cli4ai secrets set API_KEY
# Or just run the tool; cli4ai will prompt for required secrets on first run
cli4ai run my-tool fetch users/123 5. Declare Peer Dependencies
If your tool wraps a system command, declare it:
{
"peerDependencies": {
"gh": ">=2.0.0",
"ffmpeg": ">=4.0.0"
}
} cli4ai will check these and prompt the user to install if missing.
6. Know Your Paths (cwd vs tool files)
When you run a tool via cli4ai run, the tool runs with its working directory set to where you invoked cli4ai.
This makes relative paths behave the way users expect.
import { resolve } from 'path';
// When running via cli4ai, cwd is the directory where you invoked `cli4ai`.
// This means relative file paths naturally point at your project files.
const inputFile = resolve(process.cwd(), 'notes.md');
// If you need files shipped with your tool, keep them next to the entry script.
// (Bun supports import.meta.dir)
const toolDir = process.env.CLI4AI_PACKAGE_DIR ?? import.meta.dir;
const schemaFile = resolve(toolDir, 'schemas', 'output.schema.json'); Package Types
The snippets below assume you're inside a tool entry script using @cli4ai/lib
(i.e. you already created a program via cli() and you have output() available).
API Integration
Wrap external APIs (Gmail, Slack, GitHub):
// lib/api.ts
import { env } from '@cli4ai/lib/cli.ts';
const API_KEY = env('API_KEY');
export async function apiGet(endpoint: string) {
const response = await fetch(`https://api.service.com${endpoint}`, {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
// run.ts (snippet)
import { output, withErrorHandling } from '@cli4ai/lib/cli.ts';
import { apiGet } from './lib/api';
program.command('list').action(withErrorHandling(async () => {
output(await apiGet('/items'));
})); CLI Wrapper
Wrap existing CLI tools with better output:
import { $ } from 'bun';
import { output, withErrorHandling } from '@cli4ai/lib/cli.ts';
program.command('status').action(withErrorHandling(async () => {
const result = await $`gh pr status --json state,title,number`.json();
output(result);
})); Database Tool
Query databases with safe, read-only access:
import { MongoClient } from 'mongodb';
import { env, output, parseJson, withErrorHandling } from '@cli4ai/lib/cli.ts';
program.command('find')
.argument('')
.argument('[query]', 'JSON query', '{}')
.action(withErrorHandling(async (collection: string, query: string) => {
// Don't forget to declare "mongodb" in cli4ai.json dependencies.
const mongoUri = env('MONGO_URI');
const filter = parseJson>(query, 'query');
const client = new MongoClient(mongoUri);
const db = client.db();
const results = await db.collection(collection)
.find(filter)
.limit(100)
.toArray();
output(results);
await client.close();
})); 4) Run & Test
Test Locally
# Run commands directly
bun run run.ts hello "World"
bun run run.ts fetch users/123 --raw
# Install locally for testing
cli4ai add --local /path/to/my-tool -y
# Test via cli4ai
cli4ai run my-tool hello "World"
# Tool flags are supported (they pass through to your script)
cli4ai run my-tool fetch users/123 --raw
# For tool help (or when flags conflict with cli4ai), use "--"
cli4ai run my-tool fetch -- --help
Tip: cli4ai add --local installs by symlinking/copying your tool into the project.
Code changes are picked up immediately; if you change dependencies in cli4ai.json, re-run cli4ai add --local.
Test MCP Integration
# Start as MCP server
cli4ai start my-tool
# In another terminal, test with MCP client
# or add to Claude Code config and test there Publishing
Local Sharing (recommended for personal/team tools)
The easiest way to share tools is a local registry folder (a directory of tool folders):
# 1) Create a folder for your tools
mkdir -p ~/my-tools
# 2) Put your tool folders inside it (copy or symlink)
cp -R ./my-tool ~/my-tools/my-tool
# or (recommended for active development)
ln -s /full/path/to/my-tool ~/my-tools/my-tool
# 3) Add the registry directory once
cli4ai config --add-registry ~/my-tools
# 4) Now you can install by name
cli4ai add my-tool Publish to npm (official packages)
cli4ai installs tools from npm under the @cli4ai scope. If you want a tool to be installable by name
with cli4ai add my-tool, it needs to be published as @cli4ai/my-tool (requires access to that scope).
-
Make sure your published package includes
cli4ai.jsonand an executable entry script. Examplepackage.json:{ "name": "@cli4ai/my-tool", "version": "1.0.0", "description": "A description of what your tool does", "author": "your-name", "license": "MIT", "type": "module", "bin": { "my-tool": "./run.ts" }, "dependencies": { "@cli4ai/lib": "^1.0.0", "commander": "^14.0.0" }, "files": [ "run.ts", "cli4ai.json", "README.md", "LICENSE" ], "publishConfig": { "access": "public" } } -
Publish to npm:
npm publish --access public
Install from anywhere:
cli4ai add my-tool
# or (explicit scope)
cli4ai add @cli4ai/my-tool Example: Building a Weather Tool
Let's build a complete example:
1. Initialize
cli4ai init weather --template api
cd weather 2. Edit Manifest
{
"name": "weather",
"version": "1.0.0",
"description": "Get weather data for any location",
"entry": "run.ts",
"runtime": "bun",
"dependencies": {
"@cli4ai/lib": "^1.0.0",
"commander": "^14.0.0"
},
"commands": {
"current": {
"description": "Get current weather",
"args": [
{ "name": "location", "required": true, "description": "City name or coordinates" }
]
},
"forecast": {
"description": "Get weather forecast",
"args": [
{ "name": "location", "required": true },
{ "name": "days", "required": false, "description": "Number of days (1-7)" }
]
}
},
"env": {
"WEATHER_API_KEY": {
"required": true,
"secret": true,
"description": "OpenWeatherMap API key"
}
},
"mcp": {
"enabled": true
}
} 3. Implement
#!/usr/bin/env bun
import { cli, env, output, outputError, withErrorHandling } from '@cli4ai/lib/cli.ts';
const API_KEY = env('WEATHER_API_KEY');
const BASE_URL = 'https://api.openweathermap.org/data/2.5';
async function fetchWeather(endpoint: string, params: Record) {
const url = new URL(`${BASE_URL}/${endpoint}`);
url.searchParams.set('appid', API_KEY);
url.searchParams.set('units', 'metric');
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
const response = await fetch(url);
if (!response.ok) {
if (response.status === 401) outputError('AUTH_FAILED', 'Invalid API key');
if (response.status === 404) outputError('NOT_FOUND', 'Location not found');
outputError('API_ERROR', `API returned ${response.status}`);
}
return response.json();
}
const program = cli('weather', '1.0.0', 'Weather API tool');
program
.command('current')
.description('Get current weather')
.argument('', 'City name')
.action(withErrorHandling(async (location: string) => {
const data = await fetchWeather('weather', { q: location });
output({
location: data.name,
country: data.sys.country,
temperature: data.main.temp,
feels_like: data.main.feels_like,
humidity: data.main.humidity,
description: data.weather[0].description,
wind_speed: data.wind.speed
});
}));
program
.command('forecast')
.description('Get weather forecast')
.argument('', 'City name')
.argument('[days]', 'Days to forecast (1-7)', '3')
.action(withErrorHandling(async (location: string, days: string) => {
const parsedDays = Number.parseInt(days, 10);
if (!Number.isFinite(parsedDays) || parsedDays < 1 || parsedDays > 7) {
outputError('INVALID_INPUT', 'days must be an integer from 1 to 7', { days });
}
const data = await fetchWeather('forecast', { q: location, cnt: String(parsedDays * 8) });
output({
location: data.city.name,
country: data.city.country,
forecast: data.list.map((item: any) => ({
datetime: item.dt_txt,
temperature: item.main.temp,
description: item.weather[0].description
}))
});
}));
program.parse(); 4. Test
# Provide the API key (either approach works)
cli4ai secrets set WEATHER_API_KEY
# or create a .env file in the directory you'll run from:
echo 'WEATHER_API_KEY="your-key"' > .env
# Test directly
bun run run.ts current "London"
bun run run.ts forecast "Tokyo" 5
# Install into any project and test via cli4ai
cli4ai add --local /path/to/weather -y
cli4ai run weather current "New York" Next Steps
- Browse Packages - See more examples
- How It Works - Understand the architecture
- GitHub - Contribute to cli4ai