Add agent-friendly --json NDJSON output to Python CLI scripts, or scaffold a complete cli_utils package for a project. Use this skill when the user wants to make scripts machine-readable for AI agents, add --json flags, convert print statements to structured JSON, build a CLI helper library, create an open-source CLI-for-agents package, add structured logging, or make CLI output machine-readable. Also use when the user mentions NDJSON, structured CLI output, agent-friendly CLI, non-interactive scripts, or JSON I/O for automation.
npx skills add https://github.com/glebis/claude-skills --skill agent-cli
Convert Python CLI scripts from human-only output to agent-consumable NDJSON, or scaffold a complete cli_utils package ready for open-source distribution.
When the user points at a script and says "make this agent-friendly" or "add --json":
cli_utils.py if the project doesn't have oneWhen the user says "create a cli_utils package" or wants an open-source library:
json_log, json_error, die, log, add_json_flag, enable_json, is_jsonThe fundamental pattern: every script gets a --json flag. When active, all stdout becomes newline-delimited JSON (NDJSON). Each line is a self-contained JSON object with a standard envelope.
Every JSON line has at minimum:
{"event": "ready", "ts": "2026-04-30T14:00:00+00:00", "pid": 1234, "port": 8765}
event — what happened (snake_case string)ts — ISO 8601 UTC timestamp--json opt-in over default: preserves human DX, doesn't break existing scripts or habitsdie() over repeated if/else: the pattern if is_json(): json_error(); sys.exit(1) else: print(); sys.exit() appears constantly — die() collapses it to one lineWhen generating cli_utils.py, produce exactly this (adapt only if the project has specific needs):
"""Shared helpers for JSON CLI output."""
import json
import os
import sys
from datetime import datetime, timezone
_json_mode = False
def enable_json():
global _json_mode
_json_mode = True
def is_json():
return _json_mode
def json_log(event: str, **kwargs):
"""Emit one NDJSON line to stdout."""
obj = {"event": event, "ts": datetime.now(timezone.utc).isoformat(), **kwargs}
print(json.dumps(obj, default=str), flush=True)
def json_error(message: str, **kwargs):
"""Emit a structured error event."""
json_log("error", message=message, **kwargs)
def die(message: str, code: int = 1, **kwargs):
"""Print error and exit — JSON or human depending on mode."""
if _json_mode:
json_error(message, **kwargs)
else:
print(message, file=sys.stderr)
sys.exit(code)
def add_json_flag(parser):
"""Add --json flag to an argparse parser."""
parser.add_argument("--json", action="store_true",
help="NDJSON output for agent consumption")
def log(message: str, **json_kwargs):
"""Print human message normally, or emit JSON event if --json is active."""
if _json_mode:
json_log(json_kwargs.pop("event", "info"), message=message, **json_kwargs)
else:
print(message)
def json_ready(**kwargs):
"""Emit the readiness signal — only in JSON mode. Call early in daemon startup."""
if _json_mode:
json_log("ready", pid=os.getpid(), **kwargs)
Search the target script for all places that produce output or exit:
grep -n 'print(\|sys\.exit\|exit(\|input(\|os\.system.*say' TARGET.py
Categorize each hit:
log(message, event="descriptive_name")die(message)if is_json(): json_log("status", ...) else: print("\r...", end="", flush=True)if not is_json(): or add --no-interactive flagif not is_json():sys.exit("message") (writes to stderr)At the top of the script, after existing imports:
from cli_utils import add_json_flag, enable_json, is_json, json_log, log, die
In the if __name__ == "__main__" block, add to argparse:
add_json_flag(parser)
args = parser.parse_args()
if args.json:
enable_json()
Apply the categorization from Step 1. Key patterns:
Simple informational:
# Before
print(f"Connected to {device}")
# After
log(f"Connected to {device}", event="connected", device=device)
Error + exit:
# Before
print("Device not found")
sys.exit(1)
# After
die("Device not found")
Daemon readiness (first output after initialization):
# Before
print(f"Server running on port {port}")
# After — json_ready() only emits in JSON mode, so always call it + human fallback
json_ready(port=port)
log(f"Server running on port {port}", event="ready", port=port)
Status lines (\r overwrite):
# Before
print(f"\r HR {hr} RMSSD {rmssd:.1f}", end="", flush=True)
# After
if is_json():
json_log("status", hr=hr, rmssd=rmssd)
else:
print(f"\r HR {hr} RMSSD {rmssd:.1f}", end="", flush=True)
Human-only output (banners, usage examples):
if not is_json():
print("Usage: send {\"type\": \"join\", \"name\": \"Alice\"}")
python script.py --help — confirm --json flag appearspython script.py --json — confirm first line is valid JSONprint( calls — ensure each is guarded or intentionalUse snake_case, be descriptive, keep them grep-friendly:
| Category | Events |
|----------|--------|
| Lifecycle | ready, shutdown, connected, disconnected |
| Data | hr, status, metric, heartbeat |
| Errors | error, retry |
| Actions | recording_started, recording_stopped, preset_change |
| Progress | scanning, connecting, downloading, importing |
When the user wants a distributable package, scaffold this structure:
cli-utils-agent/
├── pyproject.toml
├── LICENSE # MIT by default, ask user
├── README.md
├── src/
│ └── cli_utils_agent/
│ ├── __init__.py # re-exports all public API
│ └── core.py # the implementation
├── tests/
│ ├── __init__.py
│ ├── test_json_log.py
│ ├── test_die.py
│ ├── test_log.py
│ └── test_add_json_flag.py
└── .github/
└── workflows/
└── test.yml # CI with pytest
__init__.py — re-export public API# src/cli_utils_agent/__init__.py
from .core import (
enable_json, is_json, json_log, json_error, die,
add_json_flag, log, json_ready,
)
__all__ = [
"enable_json", "is_json", "json_log", "json_error", "die",
"add_json_flag", "log", "json_ready",
]
Generate a README with: project name, one-line description, install instructions (pip install cli-utils-agent), quick usage example showing add_json_flag + enable_json + log(), API reference table listing all exports with one-line descriptions, and a link to the research background.
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "cli-utils-agent"
version = "0.1.0"
description = "Add agent-friendly --json NDJSON output to any Python CLI"
readme = "README.md"
license = "MIT"
requires-python = ">=3.10"
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Topic :: Software Development :: Libraries",
]
[project.urls]
Homepage = "https://github.com/USER/cli-utils-agent"
[tool.hatch.build.targets.wheel]
packages = ["src/cli_utils_agent"]
Use capsys for stdout capture, pytest.raises(SystemExit) for die(). Example:
# tests/test_json_log.py
import json
from cli_utils_agent import json_log, enable_json, is_json
def test_json_log_writes_ndjson(capsys):
json_log("ready", port=8765, pid=42)
line = capsys.readouterr().out.strip()
obj = json.loads(line)
assert obj["event"] == "ready"
assert obj["port"] == 8765
assert "ts" in obj
# tests/test_die.py
import json
import pytest
from cli_utils_agent import die, enable_json
from cli_utils_agent import core as _core
def test_die_human_mode(capsys):
_core._json_mode = False
with pytest.raises(SystemExit) as exc:
die("something broke")
assert exc.value.code == 1
assert "something broke" in capsys.readouterr().err
def test_die_json_mode(capsys):
_core._json_mode = True
try:
with pytest.raises(SystemExit):
die("something broke", code=10)
obj = json.loads(capsys.readouterr().out.strip())
assert obj["event"] == "error"
assert obj["message"] == "something broke"
finally:
_core._json_mode = False
Cover: json_log, json_error, die, log, add_json_flag, enable_json/is_json, json_ready.
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: pip install -e ".[dev]"
- run: pytest -v
Add dev dependencies to pyproject.toml:
[project.optional-dependencies]
dev = ["pytest>=8.0"]
After converting a script or creating a package:
--help shows --json flag--json produces valid NDJSON (every line is parseable JSON)"event": "ready""event": "error" with non-zero exit codeprint() can fire when --json is activesys.exit("message") not print()--json is NOT passedpython -m build (if package mode)Read references/best-practices.md when you need:
--schema (section 2.5)Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).
Automatically creates user-facing changelogs from git commits by analyzing commit history, categorizing changes, and transforming technical commits into clear, customer-friendly release notes. Turns hours of manual changelog writing into minutes of automated generation.
Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).
React Native and Expo best practices for building performant mobile apps. Use when building React Native components, optimizing list performance, implementing animations, or working with native modules. Triggers on tasks involving React Native, Expo, mobile performance, or native platform APIs.
React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
Next.js best practices - file conventions, RSC boundaries, data patterns, async APIs, metadata, error handling, route handlers, image/font optimization, bundling
Use when starting feature work that needs isolation from current workspace or before executing implementation plans - creates isolated git worktrees with smart directory selection and safety verification
Take glebis/agent-cli from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference pip.
Without those the skill loads but fails at the first command.