关于 agent-cli
agent-cli 可将 Python 命令行脚本改造成便于 Agent 使用的形式,也可搭建可分发的 `cli_utils` 包。它添加可选的 `--json` NDJSON 输出,同时默认保留面向人的输出。
主要功能
- 01将脚本输出改为 NDJSON 辅助函数
- 02添加可选的 `--json` 标志
- 03搭建含测试的 Python 包
- 04提供结构化日志与错误处理函数
- 05验证 JSON 输出并处理交互提示
适用场景
- 01让 Agent 能使用 Python 命令行工具
- 02为现有脚本添加机器可读输出
- 03创建可复用的 `cli_utils` 包
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ glebis/ claude-skills --skill agent-cli安装后在 Claude Code 中输入/agent-cli确认已加载。
附带文件evals/evals.jsonreferences/best-practices.md
详细介绍
Agent 友好的 CLI 构建器
将仅面向用户的 Python CLI 脚本输出转换为 Agent 可使用的 NDJSON,或搭建一个完整的 cli_utils 软件包,使其可直接开源发布。
两种模式
模式 A:转换现有脚本
当用户指定一个脚本并说“让它适合 Agent 使用”或“添加 --json”时:
- 扫描脚本中的输出位置
- 如果项目没有
cli_utils.py,则生成该文件 - 将所有输出替换为结构化辅助函数
- 验证 JSON 模式下没有原始输出泄漏
模式 B:搭建完整软件包
当用户说“创建一个 cli_utils 软件包”或想要一个开源库时:
- 搭建完整的 Python 软件包,包含 pyproject.toml、测试、许可证和 README
- 包含所有辅助函数:
json_log、json_error、die、log、add_json_flag、enable_json、is_json - 添加覆盖率完整的 pytest 测试套件
- 添加 MIT 许可证(或询问用户的偏好)
核心架构
基本模式:每个脚本都有一个 --json 标志。启用后,所有标准输出都会变为以换行符分隔的 JSON(NDJSON)。每一行都是一个独立的 JSON 对象,并带有标准封套。
NDJSON 事件封套
每行 JSON 至少包含:
{"event": "ready", "ts": "2026-04-30T14:00:00+00:00", "pid": 1234, "port": 8765}
event— 发生了什么(snake_case 字符串)ts— ISO 8601 UTC 时间戳- 其他字段由事件专属的 kwargs 指定
此设计的原因
- 选择 NDJSON 而非 JSON 数组:可逐行处理;一行错误不会破坏整个数据流;兼容 grep/jq;适用于长时间运行的进程,且内存占用低
- 默认关闭,选择启用
--json:保留面向用户的开发体验,不破坏现有脚本或使用习惯 - 使用全局模式标志,而非逐次调用时检查:启动时设置一次,所有辅助函数都会自动遵循
- 使用
die(),而非反复编写 if/else:if is_json(): json_error(); sys.exit(1) else: print(); sys.exit()这种模式会反复出现,die()可以将其简化为一行
cli_utils.py 参考实现
生成 cli_utils.py 时,必须精确输出以下内容(仅在项目有特定需求时才进行调整):
"""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)
转换脚本 — 分步指南
第 1 步:扫描输出位置
在目标脚本中搜索所有会输出内容或退出的位置:
grep -n 'print(\|sys\.exit\|exit(\|input(\|os\.system.*say' TARGET.py
对每个匹配项进行分类:
- 信息性打印 → 替换为
log(message, event="descriptive_name") - 错误并退出 → 替换为
die(message) - 带 \r 的状态行 → 替换为
if is_json(): json_log("status", ...) else: print("\r...", end="", flush=True) - 交互式 input() → 使用
if not is_json():进行保护,或添加--no-interactive标志 - 副作用(如 osascript、通知) → 使用
if not is_json():进行保护 - 导入时错误(argparse 运行前)→ 使用
sys.exit("message")(写入 stderr)
第 2 步:添加导入语句和标志
在脚本顶部的现有导入语句之后:
from cli_utils import add_json_flag, enable_json, is_json, json_log, log, die
在 if __name__ == "__main__" 代码块中,将其添加到 argparse:
add_json_flag(parser)
args = parser.parse_args()
if args.json:
enable_json()
第 3 步:替换每个输出位置
应用第 1 步中的分类。关键模式:
简单的信息性输出:
# Before
print(f"Connected to {device}")
# After
log(f"Connected to {device}", event="connected", device=device)
错误并退出:
# Before
print("Device not found")
sys.exit(1)
# After
die("Device not found")
守护进程就绪(初始化后的首次输出):
# 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)
状态行(\r 覆写):
# 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)
仅面向用户的输出(横幅、用法示例):
if not is_json():
print("Usage: send {\"type\": \"join\", \"name\": \"Alice\"}")
第 4 步:验证
- 运行
python script.py --help— 确认出现--json标志 - 运行
python script.py --json— 确认第一行是有效 JSON - 搜索剩余的原始
print(调用 — 确保每个调用都受到保护或有意保留
事件名称约定
使用 snake_case,名称应明确且便于 grep 搜索:
| 类别 | 事件 |
|---|---|
| 生命周期 | ready、shutdown、connected、disconnected |
| 数据 | hr、status、metric、heartbeat |
| 错误 | error、retry |
| 操作 | recording_started、recording_stopped、preset_change |
| 进度 | scanning、connecting、downloading、importing |
构建开源软件包
当用户需要一个可分发的软件包时,按以下结构搭建:
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 — 重新导出公共 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 模板
生成 README,包含:项目名称、一句话描述、安装说明(pip install cli-utils-agent)、快速使用示例(展示 add_json_flag + enable_json + log())、列出所有导出项及其一句话说明的 API 参考表,以及研究背景链接。
pyproject.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",
"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"]
测试套件
使用 capsys 捕获标准输出,使用 pytest.raises(SystemExit) 测试 die()。示例:
# 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
覆盖: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
将开发依赖项添加到 pyproject.toml:
[project.optional-dependencies]
dev = ["pytest>=8.0"]
检查清单 — 完成前运行
转换脚本或创建软件包后:
-
--help会显示--json标志 - 使用
--json运行时会生成有效的 NDJSON(每行都可解析为 JSON) - 守护进程输出的第一行 JSON 包含
"event": "ready" - 错误路径会输出
"event": "error",且退出代码非零 - 启用
--json时,不会触发原始print() - 导入时出错(缺少依赖项)时,使用
sys.exit("message")而不是print() - 交互式提示已添加保护
- 副作用(语音、通知)已添加保护
- 未传入
--json时,保留人类可读的输出 - 未处理的异常不会在 JSON 模式下将回溯信息泄漏到标准输出(用 try/except 包装 main,并输出 json_error)
- 测试通过(软件包模式)
- 软件包可顺利构建:
python -m build(软件包模式) - 在干净的虚拟环境中安装后可以正常导入(软件包模式)
延伸阅读
需要以下内容时,请阅读 references/best-practices.md:
- 用于存活检测的心跳模式(第 2.4 节)
- 退出代码约定和字符串错误代码(第 2.3 节)
- 使用
--schema进行架构自省(第 2.5 节) - CLI 与 MCP 的决策矩阵(第 2.7 节)
- 面向 Agent 使用的令牌效率技巧(第 5 节)