Claude
Skills
Sign in
Back

agent-cli

Included with Lifetime
$97 forever

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.

AI Agents

What this skill does


# 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:

```json
{"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):

```python
"""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:

```bash
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:
```python
from cli_utils import add_json_flag, enable_json, is_json, json_log, log, die
```

In the `if __name__ == "__main__"` block, add to argparse:
```python
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:**
```python
# Before
print(f"Connected to {device}")

# After
log(f"Connected to {device}", event="connected", device=device)
```

**Error + exit:**
```python
# Before
print("Device not found")
sys.exit(1)

# After
die("Device not found")
```

**Daemon readiness (first output after initialization):**
```python
# 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):**
```python
# 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):**
```python
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:

| 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` |

## 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

```python
# 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

```toml
[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",
  
Files: 3
Size: 25.4 KB
Complexity: 44/100
Category: AI Agents

Related in AI Agents