mcpField
A
agent-cli

agent-cli

github.com/glebis开发40177 次安装1 天前更新
代码仓库

关于 agent-cli

agent-cli 可将 Python 命令行脚本改造成便于 Agent 使用的形式,也可搭建可分发的 `cli_utils` 包。它添加可选的 `--json` NDJSON 输出,同时默认保留面向人的输出。

主要功能

  1. 01将脚本输出改为 NDJSON 辅助函数
  2. 02添加可选的 `--json` 标志
  3. 03搭建含测试的 Python 包
  4. 04提供结构化日志与错误处理函数
  5. 05验证 JSON 输出并处理交互提示

适用场景

  1. 01让 Agent 能使用 Python 命令行工具
  2. 02为现有脚本添加机器可读输出
  3. 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”时:

  1. 扫描脚本中的输出位置
  2. 如果项目没有 cli_utils.py,则生成该文件
  3. 将所有输出替换为结构化辅助函数
  4. 验证 JSON 模式下没有原始输出泄漏
模式 B:搭建完整软件包

当用户说“创建一个 cli_utils 软件包”或想要一个开源库时:

  1. 搭建完整的 Python 软件包,包含 pyproject.toml、测试、许可证和 README
  2. 包含所有辅助函数:json_log、json_error、die、log、add_json_flag、enable_json、is_json
  3. 添加覆盖率完整的 pytest 测试套件
  4. 添加 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 步:验证
  1. 运行 python script.py --help — 确认出现 --json 标志
  2. 运行 python script.py --json — 确认第一行是有效 JSON
  3. 搜索剩余的原始 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 节)

评论

支持 Markdown