Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
glebis avatar

Agent Cli

  • 52 installs
  • 339 repo stars
  • Updated August 4, 2026
  • glebis/claude-skills

Add agent-friendly --json NDJSON output to Python CLI scripts, or scaffold a full cli_utils package for machine-readable CLI I/O.

About

Converts Python CLI scripts to emit agent-consumable NDJSON via a --json flag, or scaffolds a complete cli_utils package with helpers, tests, and license. A developer uses it to make scripts machine-readable for AI agents or to build an open-source CLI-for-agents library.

  • Standard NDJSON event envelope with event, ts, and event-specific fields
  • Two modes: convert an existing script, or scaffold a full pyproject package with tests

Agent Cli by the numbers

  • 52 all-time installs (skills.sh)
  • Ranked #305 of 550 CLI & Terminal skills by installs in the Skillselion catalog
  • Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/glebis/claude-skills --skill agent-cli

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs52
repo stars339
Last updatedAugust 4, 2026
Repositoryglebis/claude-skills

What it does

Add agent-friendly --json NDJSON output to Python CLI scripts, or scaffold a full cli_utils package for machine-readable CLI I/O.

Files

SKILL.mdMarkdownGitHub ↗

Agent-Friendly CLI Builder

Convert Python CLI scripts from human-only output to agent-consumable NDJSON, or scaffold a complete cli_utils package ready for open-source distribution.

Two Modes

Mode A: Convert an existing script

When the user points at a script and says "make this agent-friendly" or "add --json":

1. Scan the script for output points 2. Generate cli_utils.py if the project doesn't have one 3. Replace all output with structured helpers 4. Verify no raw output leaks in JSON mode

Mode B: Scaffold a complete package

When the user says "create a cli_utils package" or wants an open-source library:

1. Scaffold a full Python package with pyproject.toml, tests, license, README 2. Include all helpers: json_log, json_error, die, log, add_json_flag, enable_json, is_json 3. Add pytest test suite with full coverage 4. Add MIT license (or ask user preference)

Core Architecture

The 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.

The NDJSON Event 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
  • Additional fields are event-specific kwargs

Why This Design

  • NDJSON over JSON arrays: processable line-by-line, one bad line doesn't break the stream, works with grep/jq, low memory for long-running processes
  • `--json` opt-in over default: preserves human DX, doesn't break existing scripts or habits
  • Global mode flag over per-call checks: set once at startup, every helper respects it automatically
  • `die()` 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 line

The cli_utils.py Reference Implementation

When 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)

Converting a Script — Step by Step

Step 1: Scan for output points

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:

  • Informational print → replace with log(message, event="descriptive_name")
  • Error + exit → replace with die(message)
  • Status line with \r → replace with if is_json(): json_log("status", ...) else: print("\r...", end="", flush=True)
  • Interactive input() → guard with if not is_json(): or add --no-interactive flag
  • Side effects (say, osascript, notifications) → guard with if not is_json():
  • Import-time errors (before argparse runs) → use sys.exit("message") (writes to stderr)

Step 2: Add the import and flag

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()

Step 3: Replace each output point

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\"}")

Step 4: Verify

1. Run python script.py --help — confirm --json flag appears 2. Run python script.py --json — confirm first line is valid JSON 3. Grep for remaining raw print( calls — ensure each is guarded or intentional

Event Name Conventions

Use snake_case, be descriptive, keep them grep-friendly:

CategoryEvents
Lifecycleready, shutdown, connected, disconnected
Datahr, status, metric, heartbeat
Errorserror, retry
Actionsrecording_started, recording_stopped, preset_change
Progressscanning, connecting, downloading, importing

Scaffolding an Open-Source Package

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",
]

README.md template

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.

pyproject.toml template

[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"]

Test suite

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 Actions CI

# .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"]

Checklist — Run Before Declaring Done

After converting a script or creating a package:

  • [ ] --help shows --json flag
  • [ ] Running with --json produces valid NDJSON (every line is parseable JSON)
  • [ ] First JSON line from daemons has "event": "ready"
  • [ ] Error paths emit "event": "error" with non-zero exit code
  • [ ] No raw print() can fire when --json is active
  • [ ] Import-time errors (missing deps) use sys.exit("message") not print()
  • [ ] Interactive prompts are guarded
  • [ ] Side effects (voice, notifications) are guarded
  • [ ] Human output is preserved when --json is NOT passed
  • [ ] Unhandled exceptions don't leak tracebacks to stdout in JSON mode (wrap main in try/except, emit json_error)
  • [ ] Tests pass (if package mode)
  • [ ] Package builds cleanly: python -m build (if package mode)
  • [ ] Install in clean venv and import works (if package mode)

Further Reading

Read references/best-practices.md when you need:

  • Heartbeat patterns for liveness detection (section 2.4)
  • Exit code conventions and string error codes (section 2.3)
  • Schema introspection with --schema (section 2.5)
  • CLI vs MCP decision matrix (section 2.7)
  • Token efficiency tips for agent consumption (section 5)

Related skills

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.