refactor: remove old .pi-extension layout, update .gitignore for new pi-extension dir

This commit is contained in:
Sergey Kostrov
2026-04-05 16:34:00 +03:00
committed by yuhao
parent 820939e3eb
commit 0f8b7171c6
19 changed files with 1 additions and 3228 deletions
+1 -1
View File
@@ -16,7 +16,7 @@
!/CONTRIBUTING.md
!/SECURITY.md
!/assets/
!/.pi-extension/
!/pi-extension/
!/.claude-plugin/
!/.github/
/.github/*
@@ -1,592 +0,0 @@
# Agent Harness: GUI-to-CLI for Open Source Software
## Purpose
This harness provides a standard operating procedure (SOP) and toolkit for coding
agents (Claude Code, Codex, etc.) to build powerful, stateful CLI interfaces for
open-source GUI applications. The goal: let AI agents operate software that was
designed for humans, without needing a display or mouse.
## General SOP: Turning Any GUI App into an Agent-Usable CLI
### Phase 1: Codebase Analysis
1. **Identify the backend engine** — Most GUI apps separate presentation from logic.
Find the core library/framework (e.g., MLT for Shotcut, ImageMagick for GIMP).
2. **Map GUI actions to API calls** — Every button click, drag, and menu item
corresponds to a function call. Catalog these mappings.
3. **Identify the data model** — What file formats does it use? How is project state
represented? (XML, JSON, binary, database?)
4. **Find existing CLI tools** — Many backends ship their own CLI (`melt`, `ffmpeg`,
`convert`). These are building blocks.
5. **Catalog the command/undo system** — If the app has undo/redo, it likely uses a
command pattern. These commands are your CLI operations.
### Phase 2: CLI Architecture Design
1. **Choose the interaction model**:
- **Stateful REPL** for interactive sessions (agents that maintain context)
- **Subcommand CLI** for one-shot operations (scripting, pipelines)
- **Both** (recommended) — a CLI that works in both modes
2. **Define command groups** matching the app's logical domains:
- Project management (new, open, save, close)
- Core operations (the app's primary purpose)
- Import/Export (file I/O, format conversion)
- Configuration (settings, preferences, profiles)
- Session/State management (undo, redo, history, status)
3. **Design the state model**:
- What must persist between commands? (open project, cursor position, selection)
- Where is state stored? (in-memory for REPL, file-based for CLI)
- How does state serialize? (JSON session files)
4. **Plan the output format**:
- Human-readable (tables, colors) for interactive use
- Machine-readable (JSON) for agent consumption
- Both, controlled by `--json` flag
### Phase 3: Implementation
1. **Start with the data layer** — XML/JSON manipulation of project files
2. **Add probe/info commands** — Let agents inspect before they modify
3. **Add mutation commands** — One command per logical operation
4. **Add the backend integration** — A `utils/<software>_backend.py` module that
wraps the real software's CLI. This module handles:
- Finding the software executable (`shutil.which()`)
- Invoking it with proper arguments (`subprocess.run()`)
- Error handling with clear install instructions if not found
- Example (LibreOffice):
```python
# utils/lo_backend.py
def convert_odf_to(odf_path, output_format, output_path=None, overwrite=False):
lo = find_libreoffice() # raises RuntimeError with install instructions
subprocess.run([lo, "--headless", "--convert-to", output_format, ...])
return {"output": final_path, "format": output_format, "method": "libreoffice-headless"}
```
5. **Add rendering/export** — The export pipeline calls the backend module.
Generate valid intermediate files, then invoke the real software for conversion.
6. **Add session management** — State persistence, undo/redo
**Session file locking** — Use exclusive file locking for session JSON saves
to prevent concurrent write corruption. See [`guides/session-locking.md`](guides/session-locking.md)
for the `_locked_save_json` pattern (open `"r+"`, lock, then truncate inside the lock).
7. **Add the REPL with unified skin** — Interactive mode wrapping the subcommands.
- Copy `repl_skin.py` from the plugin (`cli-anything-plugin/repl_skin.py`) into
`utils/repl_skin.py` in your CLI package
- Import and use `ReplSkin` for the REPL interface:
```python
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("<software>", version="1.0.0")
skin.print_banner() # Branded startup box (auto-detects skills/SKILL.md)
pt_session = skin.create_prompt_session() # prompt_toolkit with history + styling
line = skin.get_input(pt_session, project_name="my_project", modified=True)
skin.help(commands_dict) # Formatted help listing
skin.success("Saved") # ✓ green message
skin.error("Not found") # ✗ red message
skin.warning("Unsaved") # ⚠ yellow message
skin.info("Processing...") # ● blue message
skin.status("Key", "value") # Key-value status line
skin.table(headers, rows) # Formatted table
skin.progress(3, 10, "...") # Progress bar
skin.print_goodbye() # Styled exit message
```
- ReplSkin auto-detects `skills/SKILL.md` inside the package directory and displays
it in the banner. AI agents can read the skill file at the displayed absolute path.
- Make REPL the default behavior: use `invoke_without_command=True` on the main
Click group, and invoke the `repl` command when no subcommand is given:
```python
@click.group(invoke_without_command=True)
@click.pass_context
def cli(ctx, ...):
...
if ctx.invoked_subcommand is None:
ctx.invoke(repl, project_path=None)
```
- This ensures `cli-anything-<software>` with no arguments enters the REPL
### Phase 4: Test Planning (TEST.md - Part 1)
**BEFORE writing any test code**, create a `TEST.md` file in the
`agent-harness/cli_anything/<software>/tests/` directory. This file serves as your test plan and
MUST contain:
1. **Test Inventory Plan** — List planned test files and estimated test counts:
- `test_core.py`: XX unit tests planned
- `test_full_e2e.py`: XX E2E tests planned
2. **Unit Test Plan** — For each core module, describe what will be tested:
- Module name (e.g., `project.py`)
- Functions to test
- Edge cases to cover (invalid inputs, boundary conditions, error handling)
- Expected test count
3. **E2E Test Plan** — Describe the real-world scenarios to test:
- What workflows will be simulated?
- What real files will be generated/processed?
- What output properties will be verified?
- What format validations will be performed?
4. **Realistic Workflow Scenarios** — Detail each multi-step workflow:
- **Workflow name**: Brief title
- **Simulates**: What real-world task (e.g., "photo editing pipeline",
"podcast production", "product render setup")
- **Operations chained**: Step-by-step operations
- **Verified**: What output properties will be checked
This planning document ensures comprehensive test coverage before writing code.
### Phase 5: Test Implementation
Now write the actual test code based on the TEST.md plan:
1. **Unit tests** (`test_core.py`) — Every core function tested in isolation with
synthetic data. No external dependencies.
2. **E2E tests — intermediate files** (`test_full_e2e.py`) — Verify the project files
your CLI generates are structurally correct (valid XML, correct ZIP structure, etc.)
3. **E2E tests — true backend** (`test_full_e2e.py`) — **MUST invoke the real software.**
Create a project, export via the actual software backend, and verify the output:
- File exists and size > 0
- Correct format (PDF magic bytes `%PDF-`, DOCX/XLSX/PPTX is valid ZIP/OOXML, etc.)
- Content verification where possible (CSV contains expected data, etc.)
- **Print artifact paths** so users can manually inspect: `print(f"\n PDF: {path} ({size:,} bytes)")`
- **No graceful degradation** — if the software isn't installed, tests fail, not skip
4. **Output verification** — **Don't trust that export works just because it exits
successfully.** Verify outputs programmatically:
- Magic bytes / file format validation
- ZIP structure for OOXML formats (DOCX, XLSX, PPTX)
- Pixel-level analysis for video/images (probe frames, compare brightness)
- Audio analysis (RMS levels, spectral comparison)
- Duration/format checks against expected values
5. **CLI subprocess tests** — Test the installed CLI command as a real user/agent would.
The subprocess tests MUST also produce real final output (not just ODF intermediate).
Use the `_resolve_cli` helper to run the installed `cli-anything-<software>` command:
```python
def _resolve_cli(name):
"""Resolve installed CLI command; falls back to python -m for dev.
Set env CLI_ANYTHING_FORCE_INSTALLED=1 to require the installed command.
"""
import shutil
force = os.environ.get("CLI_ANYTHING_FORCE_INSTALLED", "").strip() == "1"
path = shutil.which(name)
if path:
print(f"[_resolve_cli] Using installed command: {path}")
return [path]
if force:
raise RuntimeError(f"{name} not found in PATH. Install with: pip install -e .")
module = name.replace("cli-anything-", "cli_anything.") + "." + name.split("-")[-1] + "_cli"
print(f"[_resolve_cli] Falling back to: {sys.executable} -m {module}")
return [sys.executable, "-m", module]
class TestCLISubprocess:
CLI_BASE = _resolve_cli("cli-anything-<software>")
def _run(self, args, check=True):
return subprocess.run(
self.CLI_BASE + args,
capture_output=True, text=True,
check=check,
)
def test_help(self):
result = self._run(["--help"])
assert result.returncode == 0
def test_project_new_json(self, tmp_dir):
out = os.path.join(tmp_dir, "test.json")
result = self._run(["--json", "project", "new", "-o", out])
assert result.returncode == 0
data = json.loads(result.stdout)
# ... verify structure
```
**Key rules for subprocess tests:**
- Always use `_resolve_cli("cli-anything-<software>")` — never hardcode
`sys.executable` or module paths directly
- Do NOT set `cwd` — installed commands must work from any directory
- Use `CLI_ANYTHING_FORCE_INSTALLED=1` in CI/release testing to ensure the
installed command (not a fallback) is being tested
- Test `--help`, `--json`, project creation, key commands, and full workflows
6. **Round-trip test** — Create project via CLI, open in GUI, verify correctness
7. **Agent test** — Have an AI agent complete a real task using only the CLI
### Phase 6: Test Documentation (TEST.md - Part 2)
After running all tests successfully, **append** to the existing TEST.md:
1. **Test Results** — Paste the full `pytest -v --tb=no` output showing all tests
passing with their names and status
2. **Summary Statistics** — Total tests, pass rate, execution time
3. **Coverage Notes** — Any gaps or areas not covered by tests
The TEST.md now serves as both the test plan (written before implementation) and
the test results documentation (appended after execution), providing a complete
record of the testing process.
### Phase 6.5: SKILL.md Generation
Generate a SKILL.md file that makes the CLI discoverable and usable by AI agents
through the skill-creator methodology. This file serves as a self-contained skill
definition that can be loaded by Claude Code or other AI assistants.
**Purpose:** SKILL.md files follow a standard format that enables AI agents to:
- Discover the CLI's capabilities
- Understand command structure and usage
- Generate correct command invocations
- Handle output programmatically
**SKILL.md Structure:**
1. **YAML Frontmatter** — Triggering metadata for skill discovery:
```yaml
---
name: "cli-anything-<software>"
description: "Brief description of what the CLI does"
---
```
2. **Markdown Body** — Installation prerequisites, command syntax, command groups,
usage examples, and agent-specific guidance (JSON output, error handling).
**Generation & Customization:** Use `skill_generator.py` to extract CLI metadata
automatically, or customize via the Jinja2 template at `templates/SKILL.md.template`.
See [`guides/skill-generation.md`](guides/skill-generation.md) for the full generation
process, template customization options, and manual generation commands.
**Output Location:** SKILL.md lives inside the Python package at
`cli_anything/<software>/skills/SKILL.md` so it is installed with `pip install`.
**Key Principles:**
- SKILL.md must be self-contained (no external dependencies for understanding)
- Include agent-specific guidance for programmatic usage
- Document `--json` flag usage for machine-readable output
- List all command groups with brief descriptions
- Provide realistic examples that demonstrate common workflows
**Skill Path in CLI Banner:**
ReplSkin auto-detects `skills/SKILL.md` inside the package directory and displays
the absolute path in the startup banner. AI agents can read the skill file at the
displayed path to learn the CLI's full capabilities.
**Package Data:** Ensure `setup.py` includes the skill file so it ships with pip:
```python
package_data={
"cli_anything.<software>": ["skills/*.md"],
},
```
### Phase 7: PyPI Publishing and Installation
After building and testing the CLI, make it installable and discoverable using
**PEP 420 namespace packages** under the shared `cli_anything` namespace.
See [`guides/pypi-publishing.md`](guides/pypi-publishing.md) for the full setup.py template,
namespace package structure, import conventions, and verification steps.
**Key rule:** `cli_anything/` has **no** `__init__.py` (namespace package). Each
sub-package (`gimp/`, `blender/`, etc.) **does** have `__init__.py`.
## Architecture Patterns & Pitfalls
### Use the Real Software — Don't Reimplement It
**This is the #1 rule.** The CLI MUST call the actual software for rendering and
export — not reimplement the software's functionality in Python.
**The anti-pattern:** Building a Pillow-based image compositor to replace GIMP,
or generating bpy scripts without ever calling Blender. This produces a toy that
can't handle real workloads and diverges from the actual software's behavior.
**The correct approach:**
1. **Use the software's CLI/scripting interface** as the backend:
- LibreOffice: `libreoffice --headless --convert-to pdf/docx/xlsx/pptx`
- Blender: `blender --background --python script.py`
- GIMP: `gimp -i -b '(script-fu-console-eval ...)'`
- Inkscape: `inkscape --actions="..." --export-filename=...`
- Shotcut/Kdenlive: `melt project.mlt -consumer avformat:output.mp4`
- Audacity: `sox` for effects processing
- OBS: `obs-websocket` protocol
2. **The software is a required dependency**, not optional. Add it to installation
instructions. The CLI is useless without the actual software.
3. **Generate valid project/intermediate files** (ODF, MLT XML, .blend, SVG, etc.)
then hand them to the real software for rendering. Your CLI is a structured
command-line interface to the software, not a replacement for it.
**Example — LibreOffice CLI export pipeline:**
```python
# 1. Build the document as a valid ODF file (our XML builder)
odf_path = write_odf(tmp_path, doc_type, project)
# 2. Convert via the REAL LibreOffice (not a reimplementation)
subprocess.run([
"libreoffice", "--headless",
"--convert-to", "pdf",
"--outdir", output_dir,
odf_path,
])
# Result: a real PDF rendered by LibreOffice's full engine
```
### The Rendering Gap
**This is the #2 pitfall.** Most GUI apps apply effects at render time via their
engine. When you build a CLI that manipulates project files directly, you must also
handle rendering — and naive approaches will silently drop effects.
**The problem:** Your CLI adds filters/effects to the project file format. But when
rendering, if you use a simple tool (e.g., ffmpeg concat demuxer), it reads raw
media files and **ignores** all project-level effects. The output looks identical to
the input. Users can't tell anything happened.
**The solution — a filter translation layer:**
1. **Best case:** Use the app's native renderer (`melt` for MLT projects). It reads
the project file and applies everything.
2. **Fallback:** Build a translation layer that converts project-format effects into
the rendering tool's native syntax (e.g., MLT filters → ffmpeg `-filter_complex`).
3. **Last resort:** Generate a render script the user can run manually.
**Priority order for rendering:** native engine → translated filtergraph → script.
### MCP Backend Pattern
For software that exposes an MCP (Model Context Protocol) server instead of a traditional
CLI (e.g., DOMShell for browser automation). See [`guides/mcp-backend.md`](guides/mcp-backend.md)
for the full backend wrapper pattern, session management, daemon mode, and example implementations.
**Use when:** no native CLI exists, software has an MCP server, or you need agent-native tool integration.
### Filter Translation Pitfalls
When translating effects between formats (e.g., MLT → ffmpeg), watch for duplicate filter
merging, interleaved stream ordering, parameter scale differences, and unmappable effects.
See [`guides/filter-translation.md`](guides/filter-translation.md) for detailed rules and examples.
### Timecode Precision
Non-integer frame rates (29.97fps) cause cumulative rounding errors. Key rules: use
`round()` not `int()`, use integer arithmetic for display, accept ±1 frame tolerance.
See [`guides/timecode-precision.md`](guides/timecode-precision.md) for the full approach.
### Output Verification Methodology
Never assume an export is correct just because it ran without errors. Verify:
```python
# Video: probe specific frames with ffmpeg
# Frame 0 for fade-in (should be near-black)
# Middle frames for color effects (compare brightness/saturation vs source)
# Last frame for fade-out (should be near-black)
# When comparing pixel values between different resolutions,
# exclude letterboxing/pillarboxing (black padding bars).
# A vertical video in a horizontal frame will have ~40% black pixels.
# Audio: check RMS levels at start/end for fades
# Compare spectral characteristics against source
```
### Testing Strategy
Four test layers with complementary purposes:
1. **Unit tests** (`test_core.py`): Synthetic data, no external dependencies. Tests
every function in isolation. Fast, deterministic, good for CI.
2. **E2E tests — native** (`test_full_e2e.py`): Tests the project file generation
pipeline (ODF structure, XML content, format validation). Verifies the
intermediate files your CLI produces are correct.
3. **E2E tests — true backend** (`test_full_e2e.py`): Invokes the **real software**
(LibreOffice, Blender, melt, etc.) to produce final output files (PDF, DOCX,
rendered images, videos). Verifies the output files:
- Exist and have size > 0
- Have correct format (magic bytes, ZIP structure, etc.)
- Contain expected content where verifiable
- **Print artifact paths** so users can manually inspect results
4. **CLI subprocess tests** (in `test_full_e2e.py`): Invokes the installed
`cli-anything-<software>` command via `subprocess.run` to run the full workflow
end-to-end: create project → add content → export via real software → verify output.
**No graceful degradation.** The real software MUST be installed. Tests must NOT
skip or fake results when the software is missing — the CLI is useless without it.
The software is a hard dependency, not optional.
**Example — true E2E test for LibreOffice:**
```python
class TestWriterToPDF:
def test_rich_writer_to_pdf(self, tmp_dir):
proj = create_document(doc_type="writer", name="Report")
add_heading(proj, text="Quarterly Report", level=1)
add_table(proj, rows=3, cols=3, data=[...])
pdf_path = os.path.join(tmp_dir, "report.pdf")
result = export(proj, pdf_path, preset="pdf", overwrite=True)
# Verify the REAL output file
assert os.path.exists(result["output"])
assert result["file_size"] > 1000 # Not suspiciously small
with open(result["output"], "rb") as f:
assert f.read(5) == b"%PDF-" # Validate format magic bytes
print(f"\n PDF: {result['output']} ({result['file_size']:,} bytes)")
class TestCLISubprocessE2E:
CLI_BASE = _resolve_cli("cli-anything-libreoffice")
def test_full_writer_pdf_workflow(self, tmp_dir):
proj_path = os.path.join(tmp_dir, "test.json")
pdf_path = os.path.join(tmp_dir, "output.pdf")
self._run(["document", "new", "-o", proj_path, "--type", "writer"])
self._run(["--project", proj_path, "writer", "add-heading", "-t", "Title"])
self._run(["--project", proj_path, "export", "render", pdf_path, "-p", "pdf", "--overwrite"])
assert os.path.exists(pdf_path)
with open(pdf_path, "rb") as f:
assert f.read(5) == b"%PDF-"
```
Run tests in force-installed mode to guarantee the real command is used:
```bash
CLI_ANYTHING_FORCE_INSTALLED=1 python3 -m pytest cli_anything/<software>/tests/ -v -s
```
The `-s` flag shows the `[_resolve_cli]` print output confirming which backend
is being used and **prints artifact paths** for manual inspection.
Real-world workflow test scenarios should include:
- Multi-segment editing (YouTube-style cut/trim)
- Montage assembly (many short clips)
- Picture-in-picture compositing
- Color grading pipelines
- Audio mixing (podcast-style)
- Heavy undo/redo stress testing
- Save/load round-trips of complex projects
- Iterative refinement (add, modify, remove, re-add)
## Principles & Rules
These are non-negotiable. Every harness MUST follow all of them.
**Backend & Rendering:**
- **The real software is a hard dependency.** The CLI MUST invoke the actual application
(LibreOffice, Blender, GIMP, etc.) for rendering and export. Do NOT reimplement
rendering in Python. Do NOT gracefully degrade to a fallback library. If the software
is not installed, error with clear install instructions.
- **Manipulate the native format directly** — Parse and modify the app's native project
files (MLT XML, ODF, SVG, etc.) as the data layer.
- **Leverage existing CLI tools** — Use `libreoffice --headless`, `blender --background`,
`melt`, `ffmpeg`, `inkscape --actions`, `sox` as subprocesses for rendering.
- **Verify rendering produces correct output** — See "The Rendering Gap" in
Architecture Patterns & Pitfalls above.
- **Every filter/effect in the registry MUST have a corresponding render mapping**
or be explicitly documented as "project-only (not rendered)".
**CLI Design:**
- **Fail loudly and clearly** — Agents need unambiguous error messages to self-correct.
- **Be idempotent where possible** — Running the same command twice should be safe.
- **Provide introspection** — `info`, `list`, `status` commands are critical for agents
to understand current state before acting.
- **JSON output mode** — Every command MUST support `--json` for machine parsing.
- **Use the unified REPL skin** — Copy `cli-anything-plugin/repl_skin.py` to
`utils/repl_skin.py` and use `ReplSkin` for banner, prompt, help, and messages.
REPL MUST be the default behavior (`invoke_without_command=True`).
**Testing:**
- **E2E tests MUST invoke the real software** and produce real output files (PDF, DOCX,
rendered images, videos). Verify output exists, has correct format (magic bytes, ZIP
structure), and print artifact paths for manual inspection. Never test only
intermediate files.
- **Every export/render function MUST be verified** with programmatic output analysis.
"It ran without errors" is not sufficient.
- **E2E tests MUST include subprocess tests** that invoke the installed
`cli-anything-<software>` command via `_resolve_cli()`. Tests must work against
the actual installed package, not just source imports.
- **Test suites MUST include real-file E2E tests**, not just unit tests with synthetic
data. Format assumptions break constantly with real media.
**Documentation:**
- **Every `cli_anything/<software>/` directory MUST contain a `README.md`** explaining
how to install the software dependency, install the CLI, run tests, and basic usage.
- **Every `cli_anything/<software>/tests/` directory MUST contain a `TEST.md`**
documenting test coverage, realistic workflows tested, and full test results output.
## Directory Structure
```
<software>/
└── agent-harness/
├── <SOFTWARE>.md # Project-specific analysis and SOP
├── setup.py # PyPI package configuration (Phase 7)
├── cli_anything/ # Namespace package (NO __init__.py here)
│ └── <software>/ # Sub-package for this CLI
│ ├── __init__.py
│ ├── __main__.py # python3 -m cli_anything.<software>
│ ├── README.md # HOW TO RUN — required
│ ├── <software>_cli.py # Main CLI entry point (Click + REPL)
│ ├── core/ # Core modules (one per domain)
│ │ ├── __init__.py
│ │ ├── project.py # Project create/open/save/info
│ │ ├── ... # Domain-specific modules
│ │ ├── export.py # Render pipeline + filter translation
│ │ └── session.py # Stateful session, undo/redo
│ ├── utils/ # Shared utilities
│ │ ├── __init__.py
│ │ ├── <software>_backend.py # Backend: invokes the real software
│ │ └── repl_skin.py # Unified REPL skin (copy from plugin)
│ └── tests/ # Test suites
│ ├── TEST.md # Test documentation and results — required
│ ├── test_core.py # Unit tests (synthetic data)
│ └── test_full_e2e.py # E2E tests (real files)
└── examples/ # Example scripts and workflows
```
**Critical:** The `cli_anything/` directory must NOT contain an `__init__.py`.
This is what makes it a PEP 420 namespace package — multiple separately-installed
PyPI packages can each contribute a sub-package under `cli_anything/` without
conflicting. For example, `cli-anything-gimp` adds `cli_anything/gimp/` and
`cli-anything-blender` adds `cli_anything/blender/`, and both coexist in the
same Python environment.
Note: This HARNESS.md is part of the cli-anything-plugin. Individual software directories reference this file — do NOT duplicate it.
## Applying This to Other Software
This same SOP applies to any GUI application:
| Software | Backend CLI | Native Format | System Package | How the CLI Uses It |
|----------|-------------|---------------|----------------|-------------------|
| LibreOffice | `libreoffice --headless` | .odt/.ods/.odp (ODF ZIP) | `apt install libreoffice` | Generate ODF → convert to PDF/DOCX/XLSX/PPTX |
| Blender | `blender --background --python` | .blend-cli.json | `apt install blender` | Generate bpy script → Blender renders to PNG/MP4 |
| GIMP | `gimp -i -b '(script-fu ...)'` | .xcf | `apt install gimp` | Script-Fu commands → GIMP processes & exports |
| Inkscape | `inkscape --actions="..."` | .svg (XML) | `apt install inkscape` | Manipulate SVG → Inkscape exports to PNG/PDF |
| Shotcut/Kdenlive | `melt` or `ffmpeg` | .mlt (XML) | `apt install melt ffmpeg` | Build MLT XML → melt/ffmpeg renders video |
| Audacity | `sox` | .aup3 | `apt install sox` | Generate sox commands → sox processes audio |
| OBS Studio | `obs-websocket` | scene.json | `apt install obs-studio` | WebSocket API → OBS captures/records |
| Browser (DOMShell) | `npx @apireno/domshell` (MCP) | Accessibility Tree (virtual FS) | `npm install -g npx` (if needed) + Chrome ext | MCP SDK → DOMShell tools → filesystem navigation |
**The software is a required dependency, not optional.** The CLI generates valid
intermediate files (ODF, MLT XML, bpy scripts, SVG) and hands them to the real
software for rendering. This is what makes the CLI actually useful — it's a
command-line interface TO the software, not a replacement for it.
The pattern is always the same: **build the data → call the real software → verify
the output**.
## Guides Reference
Detailed guides live in `guides/`. Use this table to decide which ones to read
based on the software you're building a harness for.
| Guide | Read when... | Phase |
|-------|-------------|-------|
| [`session-locking.md`](guides/session-locking.md) | Implementing session save (all harnesses) | Phase 3 |
| [`skill-generation.md`](guides/skill-generation.md) | Generating the SKILL.md file | Phase 6.5 |
| [`pypi-publishing.md`](guides/pypi-publishing.md) | Packaging and installing the CLI | Phase 7 |
| [`mcp-backend.md`](guides/mcp-backend.md) | Software has an MCP server, no native CLI | Phase 3 |
| [`filter-translation.md`](guides/filter-translation.md) | Video/audio CLI with effects that need render-time translation | Phase 3 |
| [`timecode-precision.md`](guides/timecode-precision.md) | Video/audio CLI with non-integer frame rates (29.97fps, etc.) | Phase 3, 5 |
@@ -1,125 +0,0 @@
# CLI-Anything Extension for Pi Coding Agent
This directory contains the Pi Coding Agent extension for CLI-Anything, enabling AI agents to build powerful, stateful CLI interfaces for any GUI application.
## Overview
The CLI-Anything Pi extension provides 5 slash commands that inject the HARNESS.md methodology and command specifications into the agent session. This enables the agent to build CLI harnesses for any software with a codebase.
## Installation
Pi extensions are loaded automatically when placed in the `.pi-extension/extensions/` directory. To install:
```bash
# Clone the CLI-Anything repository
git clone https://github.com/HKUDS/CLI-Anything.git
# The extension is already in place at:
# CLI-Anything/.pi-extension/extensions/cli-anything/
```
## Commands
| Command | Description |
|---------|-------------|
| `/cli-anything <path-or-repo>` | Build a complete CLI harness for any GUI application |
| `/cli-anything:refine <path> [focus]` | Refine an existing CLI harness to improve coverage |
| `/cli-anything:test <path-or-repo>` | Run tests for a CLI harness and update TEST.md |
| `/cli-anything:validate <path-or-repo>` | Validate a CLI harness against HARNESS.md standards |
| `/cli-anything:list [options]` | List all CLI-Anything tools (installed and generated) |
## Usage Examples
### Build a CLI for GIMP
```
/cli-anything ./gimp
```
### Build from a GitHub repository
```
/cli-anything https://github.com/blender/blender
```
### Refine an existing harness
```
/cli-anything:refine ./gimp "batch processing and filters"
```
### List all installed CLIs
```
/cli-anything:list
```
## Extension Structure
```
.pi-extension/extensions/cli-anything/
├── index.ts # Main extension entry point
├── HARNESS.md # Methodology documentation (source of truth)
├── README.md # This file
├── commands/ # Command specifications
│ ├── cli-anything.md # Main build command
│ ├── refine.md # Refinement command
│ ├── test.md # Test runner command
│ ├── validate.md # Validation command
│ └── list.md # List tools command
├── guides/ # Detailed implementation guides
│ ├── filter-translation.md
│ ├── mcp-backend.md
│ ├── pypi-publishing.md
│ ├── session-locking.md
│ ├── skill-generation.md
│ └── timecode-precision.md
├── scripts/ # Utility scripts
│ ├── repl_skin.py # Unified REPL interface
│ ├── setup-cli-anything.sh # Setup script
│ └── skill_generator.py # SKILL.md generator
└── templates/ # Templates
└── SKILL.md.template # Skill definition template
```
## How It Works
1. **Command Registration**: The extension registers 5 slash commands with Pi's Extension API
2. **Context Injection**: When a command is invoked, it reads HARNESS.md and the relevant command spec
3. **Message Construction**: Builds a comprehensive message with methodology, specs, and user arguments
4. **Agent Execution**: Injects the message into the agent session via `pi.sendUserMessage()`
5. **Path Remapping**: Automatically remaps container paths to local system paths
## Path Remapping
The extension handles path remapping between the containerized environment (referenced in HARNESS.md) and the local system:
| Container Path | Local Path |
|----------------|------------|
| `/root/cli-anything/<software>/` | Current working directory |
| `cli-anything-plugin/repl_skin.py` | `<extension>/scripts/repl_skin.py` |
| `~/.claude/plugins/cli-anything/` | `<extension>/` |
## Development
To modify or extend this extension:
1. Edit `index.ts` for command behavior changes
2. Edit files in `commands/` for command specification changes
3. Edit `HARNESS.md` for methodology changes
4. Edit `guides/` for implementation guide changes
## Dependencies
- `@mariozechner/pi-coding-agent` - Pi Extension API
- Node.js built-in modules: `fs`, `path`, `url`
## License
MIT License - See the main CLI-Anything repository for full license details.
## See Also
- [CLI-Anything Main Repository](https://github.com/HKUDS/CLI-Anything)
- [CLI-Hub](https://hkuds.github.io/CLI-Anything/) - Browse all community CLIs
- [CONTRIBUTING.md](../../CONTRIBUTING.md) - Contribution guidelines
@@ -1,134 +0,0 @@
# cli-anything Command
Build a complete, stateful CLI harness for any GUI application.
## CRITICAL: Read HARNESS.md First
**Before doing anything else, you MUST read `./HARNESS.md`.** It defines the complete methodology, architecture standards, and implementation patterns. Every phase below follows HARNESS.md. Do not improvise — follow the harness specification.
## Usage
```bash
/cli-anything <software-path-or-repo>
```
## Arguments
- `<software-path-or-repo>` - **Required.** Either:
- A **local path** to the software source code (e.g., `/home/user/gimp`, `./blender`)
- A **GitHub repository URL** (e.g., `https://github.com/GNOME/gimp`, `github.com/blender/blender`)
If a GitHub URL is provided, the agent clones the repo locally first, then works on the local copy.
**Note:** Software names alone (e.g., "gimp") are NOT accepted. You must provide the actual source code path or repository URL so the agent can analyze the codebase.
## What This Command Does
This command implements the complete cli-anything methodology to build a production-ready CLI harness for any GUI application. **All phases follow the standards defined in HARNESS.md.**
### Phase 0: Source Acquisition
- If `<software-path-or-repo>` is a GitHub URL, clone it to a local working directory
- Verify the local path exists and contains source code
- Derive the software name from the directory name (e.g., `/home/user/gimp` -> `gimp`)
### Phase 1: Codebase Analysis
- Analyzes the local source code
- Analyzes the backend engine and data model
- Maps GUI actions to API calls
- Identifies existing CLI tools
- Documents the architecture
### Phase 2: CLI Architecture Design
- Designs command groups matching the app's domains
- Plans the state model and output formats
- Creates the software-specific SOP document (e.g., GIMP.md)
### Phase 3: Implementation
- Creates the directory structure: `agent-harness/cli_anything/<software>/core`, `utils`, `tests`
- Implements core modules (project, session, export, etc.)
- Builds the Click-based CLI with REPL support
- Implements `--json` output mode for agent consumption
- All imports use `cli_anything.<software>.*` namespace
### Phase 4: Test Planning
- Creates `TEST.md` with comprehensive test plan
- Plans unit tests for all core modules
- Plans E2E tests with real files
- Designs realistic workflow scenarios
### Phase 5: Test Implementation
- Writes unit tests (`test_core.py`) - synthetic data, no external deps
- Writes E2E tests (`test_full_e2e.py`) - real files, full pipeline
- Implements workflow tests simulating real-world usage
- Adds output verification (pixel analysis, format validation, etc.)
- Adds `TestCLISubprocess` class with `_resolve_cli("cli-anything-<software>")`
that tests the installed command via subprocess (no hardcoded paths or CWD)
### Phase 6: Test Documentation
- Runs all tests with `pytest -v --tb=no`
- Appends full test results to `TEST.md`
- Documents test coverage and any gaps
### Phase 6.5: SKILL.md Generation
- Extracts CLI metadata using `skill_generator.py`
- Generates SKILL.md with YAML frontmatter and Markdown body
- Includes command groups, examples, and agent-specific guidance
- Outputs to `cli_anything/<software>/skills/SKILL.md` inside the Python package
- Makes the CLI discoverable and usable by AI agents
### Phase 7: PyPI Publishing and Installation
- Creates `setup.py` with `find_namespace_packages(include=["cli_anything.*"])`
- Package name: `cli-anything-<software>`, namespace: `cli_anything.<software>`
- `cli_anything/` has NO `__init__.py` (PEP 420 namespace package)
- Configures console_scripts entry point for PATH installation
- Tests local installation with `pip install -e .`
- Verifies CLI is available in PATH: `which cli-anything-<software>`
## Output Structure
```
<software-name>/
└── agent-harness/
├── <SOFTWARE>.md # Software-specific SOP
├── setup.py # PyPI package config (find_namespace_packages)
└── cli_anything/ # Namespace package (NO __init__.py)
└── <software>/ # Sub-package (HAS __init__.py)
├── README.md # Installation and usage guide
├── <software>_cli.py # Main CLI entry point
├── core/ # Core modules
│ ├── project.py
│ ├── session.py
│ ├── export.py
│ └── ...
├── skills/
│ └── SKILL.md # AI-discoverable skill definition
├── utils/ # Utilities
└── tests/
├── TEST.md # Test plan and results
├── test_core.py # Unit tests
└── test_full_e2e.py # E2E tests
```
## Example
```bash
# Build a CLI for GIMP from local source
/cli-anything /home/user/gimp
# Build from a GitHub repo
/cli-anything https://github.com/blender/blender
```
## Success Criteria
The command succeeds when:
1. All core modules are implemented and functional
2. CLI supports both one-shot commands and REPL mode
3. `--json` output mode works for all commands
4. All tests pass (100% pass rate)
5. Subprocess tests use `_resolve_cli()` and pass with `CLI_ANYTHING_FORCE_INSTALLED=1`
6. TEST.md contains both plan and results
7. README.md documents installation and usage
8. SKILL.md is generated with proper YAML frontmatter and command documentation
9. setup.py is created and local installation works
10. CLI is available in PATH as `cli-anything-<software>`
@@ -1,237 +0,0 @@
# cli-anything:list Command
List all available CLI-Anything tools (installed and generated).
## Usage
```bash
/cli-anything:list [--path <directory>] [--depth <n>] [--json]
```
## Options
- `--path <directory>` - Directory to search for generated CLIs (default: current directory)
- `--depth <n>` - Maximum recursion depth for scanning (default: unlimited). Use `0` for current directory only, `1` for one level deep, etc.
- `--json` - Output in JSON format for machine parsing
## What This Command Does
Displays all CLI-Anything tools available in the system:
### 1. Installed CLIs
Uses `importlib.metadata` to find installed `cli-anything-*` packages:
- Pattern: package name starts with `cli-anything-`
- Extracts: software name, version, entry point
```python
from importlib.metadata import distributions
installed = {}
for dist in distributions():
name = dist.metadata.get("Name", "")
if name.startswith("cli-anything-"):
software = name.replace("cli-anything-", "")
version = dist.version
# Find executable via entry points or shutil.which
executable = shutil.which(f"cli-anything-{software}")
installed[software] = {
"status": "installed",
"version": version,
"executable": executable
}
```
### 2. Generated CLIs
Uses `glob` to find local CLI directories:
- Pattern: `**/agent-harness/cli_anything/*/__init__.py` (or depth-limited variant)
- Extracts: software name, version (from setup.py), source path
- Status: `generated`
```python
from pathlib import Path
import glob
import re
search_path = args.get("path", ".")
max_depth = args.get("depth", None) # None means unlimited
generated = {}
def extract_version_from_setup(setup_path):
"""Extract version from setup.py using regex."""
try:
content = Path(setup_path).read_text()
match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
return match.group(1) if match else None
except:
return None
def build_glob_patterns(base_path, depth):
"""Build list of glob patterns for depths 0 through max_depth.
Returns multiple patterns so that --depth 2 finds tools at depth 0, 1, AND 2.
"""
base = Path(base_path)
suffix = "agent-harness/cli_anything/*/__init__.py"
if depth is None:
# Unlimited depth: use **
return [str(base / "**" / suffix)]
# Generate patterns for all depths from 0 to max_depth
patterns = []
for d in range(depth + 1):
if d == 0:
# depth 0: look in current directory
patterns.append(str(base / suffix))
else:
# depth N: look N levels deep
prefix = "/".join(["*"] * d)
patterns.append(str(base / prefix / suffix))
return patterns
patterns = build_glob_patterns(search_path, max_depth)
for pattern in patterns:
for init_file in glob.glob(pattern, recursive=True):
parts = Path(init_file).parts
# Find cli_anything/<software> pattern
for i, p in enumerate(parts):
if p == "cli_anything" and i + 1 < len(parts):
software = parts[i + 1]
# Get agent-harness directory as source
agent_harness_idx = parts.index("agent-harness") if "agent-harness" in parts else i - 1
source = str(Path(*parts[:agent_harness_idx + 2])) # up to agent-harness
# Extract version from setup.py (setup.py is in agent-harness/, not cli_anything/)
setup_path = Path(*parts[:agent_harness_idx + 1]) / "setup.py"
version = extract_version_from_setup(setup_path)
generated[software] = {
"status": "generated",
"version": version,
"executable": None,
"source": source
}
break
```
### 3. Merge Results
- Deduplicate by software name
- If both installed and generated: show `installed` status with both paths
- The `source` field shows where the generated code is (even for installed)
## Output Formats
### Table Format (default)
```
CLI-Anything Tools (found 5)
Name Status Version Source
──────────────────────────────────────────────────────────────
gimp installed 1.0.0 ./gimp/agent-harness
blender installed 1.0.0 ./blender/agent-harness
inkscape generated 1.0.0 ./inkscape/agent-harness
audacity generated 1.0.0 ./audacity/agent-harness
libreoffice generated 1.0.0 ./libreoffice/agent-harness
```
### JSON Format (--json)
```json
{
"tools": [
{
"name": "gimp",
"status": "installed",
"version": "1.0.0",
"executable": "/usr/local/bin/cli-anything-gimp",
"source": "./gimp/agent-harness"
},
{
"name": "inkscape",
"status": "generated",
"version": "1.0.0",
"executable": null,
"source": "./inkscape/agent-harness"
}
],
"total": 2,
"installed": 1,
"generated_only": 1
}
```
## Error Handling
| Scenario | Action |
|----------|--------|
| No CLIs found | Show "No CLI-Anything tools found" message |
| Invalid --path | Show error: "Path not found: <path>" |
| Permission denied | Skip directory, continue scanning, show warning |
## Implementation Steps
When this command is invoked, the agent should:
1. **Parse arguments**
- Extract `--path` value (default: `.`)
- Extract `--depth` value (default: `None` for unlimited recursion)
- Extract `--json` flag (default: false)
2. **Validate path exists**
- If `--path` specified and doesn't exist, show error and exit
3. **Scan installed CLIs**
- Use `importlib.metadata.distributions()` to find all packages
- Filter for packages starting with `cli-anything-`
- Extract name, version, find executable path
4. **Scan generated CLIs**
- Build glob pattern based on depth parameter
- Use `glob.glob(pattern, recursive=True)`
- Parse directory structure to extract software name
- Calculate relative path from current directory
5. **Merge results**
- Create dict keyed by software name
- Prefer installed data when both exist
- Keep source path from generated if available
6. **Format output**
- If `--json`: output JSON to stdout
- Otherwise: format as table with proper alignment
7. **Print results**
- Show summary line with count
- Show table or JSON
## Examples
```bash
# List all tools in current directory (unlimited depth)
/cli-anything:list
# List tools with depth limit (only scan 2 levels deep)
/cli-anything:list --depth 2
# List tools in current directory only (no recursion)
/cli-anything:list --depth 0
# List tools with JSON output
/cli-anything:list --json
# Search a specific directory with depth limit
/cli-anything:list --path /projects/my-tools --depth 3
# Combined
/cli-anything:list --path ./output --depth 2 --json
```
## Notes
- `--depth` controls how many directory levels to descend from the search path
- Default depth is unlimited (`**` glob pattern)
- CLI-Anything tools typically need at least 3-4 levels to find `agent-harness/cli_anything/software/__init__.py`
- Relative paths are preferred for readability
- The command should work without any external dependencies beyond Python stdlib
@@ -1,104 +0,0 @@
# cli-anything:refine Command
Refine an existing CLI harness to improve coverage of the software's functions and usage patterns.
## CRITICAL: Read HARNESS.md First
**Before refining, read `./HARNESS.md`.** All new commands and tests must follow the same standards as the original build. HARNESS.md is the single source of truth for architecture, patterns, and quality requirements.
## Usage
```bash
/cli-anything:refine <software-path> [focus]
```
## Arguments
- `<software-path>` - **Required.** Local path to the software source code (e.g., `/home/user/gimp`, `./blender`). Must be the same source tree used during the original build.
**Note:** Only local paths are accepted. If you need to work from a GitHub repo, clone it first with `/cli-anything`, then refine.
- `[focus]` - **Optional.** A natural-language description of the functionality area to focus on. When provided, the agent skips broad gap analysis and instead targets the specified capability area.
Examples:
- `/cli-anything:refine /home/user/shotcut "vid-in-vid and picture-in-picture features"`
- `/cli-anything:refine /home/user/gimp "all batch processing and scripting filters"`
- `/cli-anything:refine /home/user/blender "particle systems and physics simulation"`
- `/cli-anything:refine /home/user/inkscape "path boolean operations and clipping"`
When `[focus]` is provided:
- Step 2 (Analyze Software Capabilities) narrows to only the specified area
- Step 3 (Gap Analysis) compares only the focused capabilities against current coverage
- The agent should still present findings before implementing, but scoped to the focus area
## What This Command Does
This command is used **after** a CLI harness has already been built with `/cli-anything`. It analyzes gaps between the software's full capabilities and what the current CLI covers, then iteratively expands coverage. If a `[focus]` is given, the agent narrows its analysis and implementation to that specific functionality area.
### Step 1: Inventory Current Coverage
- Read the existing CLI entry point (`<software>_cli.py`) and all core modules
- List every command, subcommand, and option currently implemented
- Read the existing test suite to understand what's tested
- Build a coverage map: `{ function_name: covered | not_covered }`
### Step 2: Analyze Software Capabilities
- Re-scan the software source at `<software-path>`
- Identify all public APIs, CLI tools, scripting interfaces, and batch-mode operations
- Focus on functions that produce observable output (renders, exports, transforms, conversions)
- Categorize by domain (e.g., for GIMP: filters, color adjustments, layer ops, selection tools)
### Step 3: Gap Analysis
- Compare current CLI coverage against the software's full capability set
- Prioritize gaps by:
1. **High impact** — commonly used functions missing from the CLI
2. **Easy wins** — functions with simple APIs that can be wrapped quickly
3. **Composability** — functions that unlock new workflows when combined with existing commands
- Present the gap report to the user and confirm which gaps to address
### Step 4: Implement New Commands
- Add new commands/subcommands to the CLI for the selected gaps
- Follow the same patterns as existing commands (as defined in HARNESS.md):
- Click command groups
- `--json` output support
- Session state integration
- Error handling with `handle_error`
- Add corresponding core module functions in `core/` or `utils/`
### Step 5: Expand Tests
- Add unit tests for every new function in `test_core.py`
- Add E2E tests for new commands in `test_full_e2e.py`
- Add workflow tests that combine new commands with existing ones
- Run all tests (old + new) to ensure no regressions
### Step 6: Update Documentation
- Update `README.md` with new commands and usage examples
- Update `TEST.md` with new test results
- Update the SOP document (`<SOFTWARE>.md`) with new coverage notes
## Example
```bash
# Broad refinement — agent finds gaps across all capabilities
/cli-anything:refine /home/user/gimp
# Focused refinement — agent targets a specific functionality area
/cli-anything:refine /home/user/shotcut "vid-in-vid and picture-in-picture compositing"
/cli-anything:refine /home/user/gimp "batch processing and Script-Fu filters"
/cli-anything:refine /home/user/blender "particle systems and physics simulation"
/cli-anything:refine /home/user/inkscape "path boolean operations and clipping masks"
```
## Success Criteria
- All existing tests still pass (no regressions)
- New commands follow the same architectural patterns (per HARNESS.md)
- New tests achieve 100% pass rate
- Coverage meaningfully improved (new functions exposed via CLI)
- Documentation updated to reflect changes
## Notes
- Refine is incremental — run it multiple times to steadily expand coverage
- Each run should focus on a coherent set of related functions rather than trying to cover everything at once
- The agent should present the gap analysis before implementing, so the user can steer priorities
- Refine never removes existing commands — it only adds or enhances
@@ -1,73 +0,0 @@
# cli-anything:test Command
Run tests for a CLI harness and update TEST.md with results.
## CRITICAL: Read HARNESS.md First
**Before running tests, read `./HARNESS.md`.** It defines the test standards, expected structure, and what constitutes a passing test suite.
## Usage
```bash
/cli-anything:test <software-path-or-repo>
```
## Arguments
- `<software-path-or-repo>` - **Required.** Either:
- A **local path** to the software source code (e.g., `/home/user/gimp`, `./blender`)
- A **GitHub repository URL** (e.g., `https://github.com/GNOME/gimp`, `github.com/blender/blender`)
If a GitHub URL is provided, the agent clones the repo locally first, then works on the local copy.
The software name is derived from the directory name. The agent locates the CLI harness at `/root/cli-anything/<software-name>/agent-harness/`.
## What This Command Does
1. **Locates the CLI** - Finds the CLI harness based on the software path
2. **Runs pytest** - Executes tests with `-v -s --tb=short`
3. **Captures output** - Saves full test results
4. **Verifies subprocess backend** - Confirms `[_resolve_cli] Using installed command:` appears in output
5. **Updates TEST.md** - Appends results to the Test Results section
6. **Reports status** - Shows pass/fail summary
## Test Output Format
The command appends to TEST.md:
```markdown
## Test Results
Last run: 2024-03-05 14:30:00
```
[full pytest -v --tb=no output]
```
**Summary**: 103 passed in 3.05s
```
## Example
```bash
# Run all tests for GIMP CLI
/cli-anything:test /home/user/gimp
# Run tests for Blender from GitHub
/cli-anything:test https://github.com/blender/blender
```
## Success Criteria
- All tests pass (100% pass rate)
- TEST.md is updated with full results
- No test failures or errors
- `[_resolve_cli]` output confirms installed command path
## Failure Handling
If tests fail:
1. Shows which tests failed
2. Does NOT update TEST.md (keeps previous passing results)
3. Suggests fixes based on error messages
4. Offers to re-run after fixes
@@ -1,123 +0,0 @@
# cli-anything:validate Command
Validate a CLI harness against HARNESS.md standards and best practices.
## CRITICAL: Read HARNESS.md First
**Before validating, read `./HARNESS.md`.** It is the single source of truth for all validation checks below. Every check in this command maps to a requirement in HARNESS.md.
## Usage
```bash
/cli-anything:validate <software-path-or-repo>
```
## Arguments
- `<software-path-or-repo>` - **Required.** Either:
- A **local path** to the software source code (e.g., `/home/user/gimp`, `./blender`)
- A **GitHub repository URL** (e.g., `https://github.com/GNOME/gimp`, `github.com/blender/blender`)
If a GitHub URL is provided, the agent clones the repo locally first, then works on the local copy.
The software name is derived from the directory name. The agent locates the CLI harness at `/root/cli-anything/<software-name>/agent-harness/`.
## What This Command Validates
### 1. Directory Structure
- `agent-harness/cli_anything/<software>/` exists (namespace sub-package)
- `cli_anything/` has NO `__init__.py` (PEP 420 namespace package)
- `<software>/` HAS `__init__.py` (regular sub-package)
- `core/`, `utils/`, `tests/` subdirectories present
- `setup.py` in agent-harness/ uses `find_namespace_packages`
### 2. Required Files
- `README.md` - Installation and usage guide
- `<software>_cli.py` - Main CLI entry point
- `core/project.py` - Project management
- `core/session.py` - Undo/redo
- `core/export.py` - Rendering/export
- `tests/TEST.md` - Test plan and results
- `tests/test_core.py` - Unit tests
- `tests/test_full_e2e.py` - E2E tests
- `../<SOFTWARE>.md` - Software-specific SOP
### 3. CLI Implementation Standards
- Uses Click framework
- Has command groups (not flat commands)
- Implements `--json` flag for machine-readable output
- Implements `--project` flag for project file
- Has `handle_error` decorator for consistent error handling
- Has REPL mode
- Has global session state
### 4. Core Module Standards
- `project.py` has: create, open, save, info, list_profiles
- `session.py` has: Session class with undo/redo/snapshot
- `export.py` has: render function and EXPORT_PRESETS
- All modules have proper docstrings
- All functions have type hints
### 5. Test Standards
- `TEST.md` has both plan (Part 1) and results (Part 2)
- Unit tests use synthetic data only
- E2E tests use real files
- Workflow tests simulate real-world scenarios
- `test_full_e2e.py` has a `TestCLISubprocess` class
- `TestCLISubprocess` uses `_resolve_cli("cli-anything-<software>")` (no hardcoded paths)
- `_resolve_cli` prints which backend is used and supports `CLI_ANYTHING_FORCE_INSTALLED`
- Subprocess `_run` does NOT set `cwd` (installed commands work from any directory)
- All tests pass (100% pass rate)
### 6. Documentation Standards
- `README.md` has: installation, usage, command reference, examples
- `<SOFTWARE>.md` has: architecture analysis, command map, rendering gap assessment
- No duplicate `HARNESS.md` (should reference plugin's HARNESS.md)
- All commands documented with examples
### 7. PyPI Packaging Standards
- `setup.py` uses `find_namespace_packages(include=["cli_anything.*"])`
- Package name follows `cli-anything-<software>` convention
- Entry point: `cli-anything-<software>=cli_anything.<software>.<software>_cli:main`
- `cli_anything/` has NO `__init__.py` (namespace package rule)
- All imports use `cli_anything.<software>.*` prefix
- Dependencies listed in install_requires
- Python version requirement specified (>=3.10)
### 8. Code Quality
- No syntax errors
- No import errors
- Follows PEP 8 style
- No hardcoded paths (uses relative paths or config)
- Proper error handling (no bare `except:`)
## Validation Report
The command generates a detailed report:
```
CLI Harness Validation Report
Software: gimp
Path: /root/cli-anything/gimp/agent-harness/cli_anything/gimp
Directory Structure (5/5 checks passed)
Required Files (9/9 files present)
CLI Implementation (7/7 standards met)
Core Modules (5/5 standards met)
Test Standards (10/10 standards met)
Documentation (4/4 standards met)
PyPI Packaging (7/7 standards met)
Code Quality (5/5 checks passed)
Overall: PASS (52/52 checks)
```
## Example
```bash
# Validate GIMP CLI
/cli-anything:validate /home/user/gimp
# Validate from GitHub repo
/cli-anything:validate https://github.com/blender/blender
```
@@ -1,26 +0,0 @@
# Filter Translation Pitfalls
When translating effects between formats (e.g., MLT → ffmpeg), watch for these common issues.
## Duplicate Filter Types
Some tools (ffmpeg) don't allow the same filter twice in a chain. If your project has both `brightness` and `saturation` filters, and both map to ffmpeg's `eq=`, you must **merge** them into a single `eq=brightness=X:saturation=Y`.
## Ordering Constraints
ffmpeg's `concat` filter requires **interleaved** stream ordering:
`[v0][a0][v1][a1][v2][a2]`, NOT grouped `[v0][v1][v2][a0][a1][a2]`.
The error message ("media type mismatch") is cryptic if you don't know this.
## Parameter Space Differences
Effect parameters often use different scales:
- MLT brightness `1.15` = +15%
- ffmpeg `eq=brightness=0.06` on a -1..1 scale
Document every mapping explicitly.
## Unmappable Effects
Some effects have no equivalent in the render tool. Handle gracefully (warn, skip) rather than crash.
@@ -1,64 +0,0 @@
# MCP Backend Pattern
For services that expose an MCP (Model Context Protocol) server instead of a traditional CLI.
## When to Use
- The software has an official or community MCP server
- No native CLI exists, or MCP provides better functionality
- You want to integrate AI/agent tools that speak MCP protocol
**Use case:** When the software provides an MCP server instead of a traditional CLI.
Example: DOMShell provides browser automation via MCP tools.
## Backend Wrapper (`utils/<service>_backend.py`)
```python
import asyncio
from typing import Any
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def _call_tool(tool_name: str, arguments: dict) -> Any:
"""Call an MCP tool."""
server_params = StdioServerParameters(
command="npx",
args=["@apireno/domshell"]
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool(tool_name, arguments)
return result
def is_available() -> bool:
"""Check if MCP server is available."""
# Try to spawn and verify
...
# Sync wrappers for each tool
def ls(path: str = "/") -> dict:
"""List directory contents."""
return asyncio.run(_call_tool("domshell_ls", {"path": path}))
```
## Session Management
- MCP server spawns per command (stateless from server perspective)
- CLI maintains state (URL, working directory, navigation history)
- Each command re-spawns the MCP server process
## Daemon Mode (Optional)
- Spawn MCP server once, reuse connection for multiple commands
- Reduces latency for interactive use
- Requires explicit start/stop or `--daemon` flag
## Dependencies
Add `mcp>=0.1.0` to `install_requires`.
## Example Implementations
- `browser/agent-harness` — DOMShell MCP server for browser automation
- See: https://github.com/HKUDS/CLI-Anything/tree/main/browser/agent-harness
@@ -1,117 +0,0 @@
# PyPI Publishing and Installation (Phase 7)
After building and testing the CLI, make it installable and discoverable.
All cli-anything CLIs use **PEP 420 namespace packages** under the shared
`cli_anything` namespace. This allows multiple CLI packages to be installed
side-by-side in the same Python environment without conflicts.
## 1. Package Structure
```
agent-harness/
├── setup.py
└── cli_anything/ # NO __init__.py here (namespace package)
└── <software>/ # e.g., gimp, blender, audacity
├── __init__.py # HAS __init__.py (regular sub-package)
├── <software>_cli.py
├── core/
├── utils/
└── tests/
```
The key rule: `cli_anything/` has **no** `__init__.py`. Each sub-package
(`gimp/`, `blender/`, etc.) **does** have `__init__.py`. This is what
enables multiple packages to contribute to the same namespace.
## 2. setup.py Template
Create `setup.py` in the `agent-harness/` directory:
```python
from setuptools import setup, find_namespace_packages
setup(
name="cli-anything-<software>",
version="1.0.0",
packages=find_namespace_packages(include=["cli_anything.*"]),
install_requires=[
"click>=8.0.0",
"prompt-toolkit>=3.0.0",
# Add Python library dependencies here
],
entry_points={
"console_scripts": [
"cli-anything-<software>=cli_anything.<software>.<software>_cli:main",
],
},
python_requires=">=3.10",
)
```
**Important details:**
- Use `find_namespace_packages`, NOT `find_packages`
- Use `include=["cli_anything.*"]` to scope discovery
- Entry point format: `cli_anything.<software>.<software>_cli:main`
- The **system package** (LibreOffice, Blender, etc.) is a **hard dependency**
that cannot be expressed in `install_requires`. Document it in README.md and
have the backend module raise a clear error with install instructions:
```python
# In utils/<software>_backend.py
def find_<software>():
path = shutil.which("<software>")
if path:
return path
raise RuntimeError(
"<Software> is not installed. Install it with:\n"
" apt install <software> # Debian/Ubuntu\n"
" brew install <software> # macOS"
)
```
## 3. Import Convention
All imports use the `cli_anything.<software>` prefix:
```python
from cli_anything.gimp.core.project import create_project
from cli_anything.gimp.core.session import Session
from cli_anything.blender.core.scene import create_scene
```
## 4. Verification Steps
**Test local installation:**
```bash
cd /root/cli-anything/<software>/agent-harness
pip install -e .
```
**Verify PATH installation:**
```bash
which cli-anything-<software>
cli-anything-<software> --help
```
**Run tests against the installed command:**
```bash
cd /root/cli-anything/<software>/agent-harness
CLI_ANYTHING_FORCE_INSTALLED=1 python3 -m pytest cli_anything/<software>/tests/ -v -s
```
The output must show `[_resolve_cli] Using installed command: /path/to/cli-anything-<software>`
confirming subprocess tests ran against the real installed binary, not a module fallback.
**Verify namespace works across packages** (when multiple CLIs installed):
```python
import cli_anything.gimp
import cli_anything.blender
# Both resolve to their respective source directories
```
## Why Namespace Packages
- Multiple CLIs coexist in the same Python environment without conflicts
- Clean, organized imports under a single `cli_anything` namespace
- Each CLI is independently installable/uninstallable via pip
- Agents can discover all installed CLIs via `cli_anything.*`
- Standard Python packaging — no hacks or workarounds
@@ -1,39 +0,0 @@
# Session File Locking
When saving session JSON, use exclusive file locking to prevent concurrent writes from corrupting data.
## The Problem
Never use bare `open("w") + json.dump()``open("w")` truncates the file before any lock can be acquired.
## The Solution: `_locked_save_json`
Open with `"r+"`, lock, then truncate inside the lock:
```python
def _locked_save_json(path, data, **dump_kwargs) -> None:
"""Atomically write JSON with exclusive file locking."""
try:
f = open(path, "r+") # no truncation on open
except FileNotFoundError:
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
f = open(path, "w") # first save — file doesn't exist yet
with f:
_locked = False
try:
import fcntl
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
_locked = True
except (ImportError, OSError):
pass # Windows / unsupported FS — proceed unlocked
try:
f.seek(0)
f.truncate() # truncate INSIDE the lock
json.dump(data, f, **dump_kwargs)
f.flush()
finally:
if _locked:
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
```
Copy this pattern into `core/session.py` for all session saves.
@@ -1,115 +0,0 @@
# SKILL.md Generation (Phase 6.5)
Generate a SKILL.md file that makes the CLI discoverable and usable by AI agents
through the skill-creator methodology. This file serves as a self-contained skill
definition that can be loaded by Claude Code or other AI assistants.
## Purpose
SKILL.md files follow a standard format that enables AI agents to:
- Discover the CLI's capabilities
- Understand command structure and usage
- Generate correct command invocations
- Handle output programmatically
## SKILL.md Structure
### 1. YAML Frontmatter — Triggering metadata for skill discovery:
```yaml
---
name: "cli-anything-<software>"
description: "Brief description of what the CLI does"
---
```
### 2. Markdown Body — Usage instructions including:
- Installation prerequisites
- Basic command syntax
- Command groups and their functions
- Usage examples
- Agent-specific guidance (JSON output, error handling)
## Generation Process
### 1. Extract CLI metadata using `skill_generator.py`:
```python
from skill_generator import generate_skill_file
skill_path = generate_skill_file(
harness_path="/path/to/agent-harness"
)
# Default output: cli_anything/<software>/skills/SKILL.md
```
### 2. The generator automatically extracts:
- Software name and version from setup.py
- Command groups from the CLI file (Click decorators)
- Documentation from README.md
- System package requirements
### 3. Customize the template (optional):
- Default template: `templates/SKILL.md.template`
- Uses Jinja2 placeholders for dynamic content
- Can be extended for software-specific sections
## Output Location
SKILL.md is generated inside the Python package so it is installed with `pip install`:
```
<software>/
└── agent-harness/
└── cli_anything/
└── <software>/
└── skills/
└── SKILL.md
```
## Manual Generation
```bash
cd cli-anything-plugin
python skill_generator.py /path/to/software/agent-harness
```
## Integration with CLI Build
The SKILL.md generation should be run after Phase 6 (Test Documentation) completes
successfully, ensuring the CLI is fully documented and tested before creating the
skill definition.
## Key Principles
- SKILL.md must be self-contained (no external dependencies for understanding)
- Include agent-specific guidance for programmatic usage
- Document `--json` flag usage for machine-readable output
- List all command groups with brief descriptions
- Provide realistic examples that demonstrate common workflows
## Skill Path in CLI Banner
ReplSkin auto-detects `skills/SKILL.md` inside the package and displays the absolute
path in the startup banner. AI agents can read the file at the displayed path:
```python
# In the REPL initialization (e.g., shotcut_cli.py)
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("<software>", version="1.0.0")
skin.print_banner() # Auto-detects and displays: ◇ Skill: /path/to/cli_anything/<software>/skills/SKILL.md
```
## Package Data
Ensure `setup.py` includes the skill file as package data so it is installed with pip:
```python
package_data={
"cli_anything.<software>": ["skills/*.md"],
},
```
@@ -1,22 +0,0 @@
# Timecode Precision
Non-integer frame rates (29.97fps = 30000/1001) cause cumulative rounding errors. Follow these rules to avoid drift.
## Use `round()`, Not `int()`
For float-to-frame conversion:
- `int(9000 * 29.97)`**wrong**, truncates and loses frames
- `round(9000 * 29.97)`**correct**, gets the right answer
## Use Integer Arithmetic for Timecode Display
Convert frames → total milliseconds via:
```python
total_ms = round(frames * fps_den * 1000 / fps_num)
```
Then decompose with integer division. Avoid intermediate floats that drift over long durations.
## Accept ±1 Frame Tolerance
In roundtrip tests at non-integer FPS, exact equality is mathematically impossible. Accept ±1 frame tolerance.
@@ -1,179 +0,0 @@
/**
* CLI-Anything Extension for Pi Coding Agent
*
* Provides 5 slash commands that inject HARNESS.md methodology + command specs
* into the agent session via pi.sendUserMessage(), enabling the agent to build
* CLI harnesses for any GUI application.
*
* Commands:
* /cli-anything <path-or-repo> - Build a complete CLI harness
* /cli-anything:refine <path> [focus] - Refine an existing CLI harness
* /cli-anything:test <path-or-repo> - Run tests for a CLI harness
* /cli-anything:validate <path-or-repo> - Validate a CLI harness
* /cli-anything:list [options] - List all CLI-Anything tools
*
* Asset files are self-contained in the extension directory.
*/
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
// Resolve extension directory for asset loading
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/**
* Read an asset file relative to the extension's assets/ directory.
*/
function readAsset(...paths: string[]): string {
const fullPath = join(__dirname, ...paths);
return readFileSync(fullPath, "utf-8");
}
/**
* Construct the message payload injected into the agent session.
* Bundles HARNESS.md + command spec + user args into a single user message.
*/
function buildCommandMessage(
commandName: string,
commandMd: string,
userArgs: string,
): string {
const harnessMd = readAsset("HARNESS.md");
const guidesDir = join(__dirname, "guides");
const scriptsDir = join(__dirname, "scripts");
const templatesDir = join(__dirname, "templates");
return `[CLI-Anything Command: ${commandName}]
## CRITICAL: HARNESS.md — Read First
${harnessMd}
## Command Specification: ${commandName}
${commandMd}
## User Arguments
\`${userArgs}\`
## Extension Asset Paths
The following resources are available on this system. Use the \`read\` tool to access them when needed:
- Guides directory: \`${guidesDir}/\` — when HARNESS.md references guides (e.g. "See guides/session-locking.md"), read them from here
- Scripts directory: \`${scriptsDir}/\` — contains \`skill_generator.py\`, \`repl_skin.py\`, and \`setup-cli-anything.sh\`
- Templates directory: \`${templatesDir}/\` — contains \`SKILL.md.template\`
## Path Remapping Rules
The command specs and HARNESS.md were written for a containerized environment. Apply these remapping rules:
1. \`/root/cli-anything/<software>/\` → use the current working directory (\`cwd\`). The software source is wherever the user specified in the arguments.
2. \`cli-anything-plugin/repl_skin.py\` → use \`${scriptsDir}/repl_skin.py\`
3. \`cli-anything-plugin/skill_generator.py\` → use \`${scriptsDir}/skill_generator.py\`
4. \`~/.claude/plugins/cli-anything/\` → use \`${__dirname}/\`
5. All relative paths in HARNESS.md (e.g. \`guides/...\`, \`templates/...\`) resolve against the asset paths above, NOT the working directory.
---
You are executing the /${commandName} command. Follow the HARNESS.md methodology and command specification precisely. Read HARNESS.md FIRST before taking any action. When you encounter references to guides, scripts, or templates, read them from the directories listed above. Apply the Path Remapping Rules for any hardcoded paths found in the specs.`;
}
/**
* Inject command context into the agent session via sendUserMessage.
* This triggers a full agent turn with access to all tools.
*/
function injectCommandContext(
pi: ExtensionAPI,
commandName: string,
commandMdPath: string,
userArgs: string,
): void {
const commandMd = readAsset("commands", commandMdPath);
const message = buildCommandMessage(commandName, commandMd, userArgs);
pi.sendUserMessage(message);
}
export default function cliAnythingExtension(pi: ExtensionAPI) {
// ─── /cli-anything <path-or-repo> ─────────────────────────────────
pi.registerCommand("cli-anything", {
description: "Build a complete CLI harness for any GUI application",
handler: async (args, ctx) => {
const trimmed = args.trim();
if (!trimmed) {
ctx.ui.notify(
"Usage: /cli-anything <path-or-repo>\n\nProvide a local path to software source code or a GitHub repository URL.",
"warning",
);
return;
}
injectCommandContext(pi, "cli-anything", "cli-anything.md", trimmed);
},
});
// ─── /cli-anything:refine <path> [focus] ──────────────────────────
pi.registerCommand("cli-anything:refine", {
description: "Refine an existing CLI harness to improve coverage",
handler: async (args, ctx) => {
const trimmed = args.trim();
if (!trimmed) {
ctx.ui.notify(
'Usage: /cli-anything:refine <software-path> [focus]\n\nExample: /cli-anything:refine /home/user/gimp "batch processing filters"',
"warning",
);
return;
}
injectCommandContext(pi, "cli-anything:refine", "refine.md", trimmed);
},
});
// ─── /cli-anything:test <path-or-repo> ────────────────────────────
pi.registerCommand("cli-anything:test", {
description: "Run tests for a CLI harness and update TEST.md",
handler: async (args, ctx) => {
const trimmed = args.trim();
if (!trimmed) {
ctx.ui.notify(
"Usage: /cli-anything:test <software-path-or-repo>\n\nProvide a local path to software source code or a GitHub repository URL.",
"warning",
);
return;
}
injectCommandContext(pi, "cli-anything:test", "test.md", trimmed);
},
});
// ─── /cli-anything:validate <path-or-repo> ────────────────────────
pi.registerCommand("cli-anything:validate", {
description: "Validate a CLI harness against HARNESS.md standards",
handler: async (args, ctx) => {
const trimmed = args.trim();
if (!trimmed) {
ctx.ui.notify(
"Usage: /cli-anything:validate <software-path-or-repo>\n\nProvide a local path to software source code or a GitHub repository URL.",
"warning",
);
return;
}
injectCommandContext(pi, "cli-anything:validate", "validate.md", trimmed);
},
});
// ─── /cli-anything:list [--path] [--depth] [--json] ───────────────
pi.registerCommand("cli-anything:list", {
description: "List all CLI-Anything tools (installed and generated)",
getArgumentCompletions: (prefix: string) => {
const flags = ["--json", "--path ", "--depth "];
const filtered = flags.filter((f) => f.startsWith(prefix));
return filtered.length > 0 ? filtered.map((f) => ({ value: f, label: f })) : null;
},
handler: async (args, ctx) => {
// Parse optional flags, pass everything to the agent
const trimmed = args.trim();
// No validation needed — the agent handles --path, --depth, --json parsing
injectCommandContext(pi, "cli-anything:list", "list.md", trimmed || "(no arguments — scan current directory)");
},
});
}
@@ -1,521 +0,0 @@
"""cli-anything REPL Skin — Unified terminal interface for all CLI harnesses.
Copy this file into your CLI package at:
cli_anything/<software>/utils/repl_skin.py
Usage:
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("shotcut", version="1.0.0")
skin.print_banner() # auto-detects skills/SKILL.md inside the package
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
skin.warning("Unsaved changes")
skin.info("Processing 24 clips...")
skin.status("Track 1", "3 clips, 00:02:30")
skin.table(headers, rows)
skin.print_goodbye()
"""
import os
import sys
# ── ANSI color codes (no external deps for core styling) ──────────────
_RESET = "\033[0m"
_BOLD = "\033[1m"
_DIM = "\033[2m"
_ITALIC = "\033[3m"
_UNDERLINE = "\033[4m"
# Brand colors
_CYAN = "\033[38;5;80m" # cli-anything brand cyan
_CYAN_BG = "\033[48;5;80m"
_WHITE = "\033[97m"
_GRAY = "\033[38;5;245m"
_DARK_GRAY = "\033[38;5;240m"
_LIGHT_GRAY = "\033[38;5;250m"
# Software accent colors — each software gets a unique accent
_ACCENT_COLORS = {
"gimp": "\033[38;5;214m", # warm orange
"blender": "\033[38;5;208m", # deep orange
"inkscape": "\033[38;5;39m", # bright blue
"audacity": "\033[38;5;33m", # navy blue
"libreoffice": "\033[38;5;40m", # green
"obs_studio": "\033[38;5;55m", # purple
"kdenlive": "\033[38;5;69m", # slate blue
"shotcut": "\033[38;5;35m", # teal green
}
_DEFAULT_ACCENT = "\033[38;5;75m" # default sky blue
# Status colors
_GREEN = "\033[38;5;78m"
_YELLOW = "\033[38;5;220m"
_RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
_ICON = f"{_CYAN}{_BOLD}{_RESET}"
_ICON_SMALL = f"{_CYAN}{_RESET}"
# ── Box drawing characters ────────────────────────────────────────────
_H_LINE = ""
_V_LINE = ""
_TL = ""
_TR = ""
_BL = ""
_BR = ""
_T_DOWN = ""
_T_UP = ""
_T_RIGHT = ""
_T_LEFT = ""
_CROSS = ""
def _strip_ansi(text: str) -> str:
"""Remove ANSI escape codes for length calculation."""
import re
return re.sub(r"\033\[[^m]*m", "", text)
def _visible_len(text: str) -> int:
"""Get visible length of text (excluding ANSI codes)."""
return len(_strip_ansi(text))
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
Provides consistent branding, prompts, and message formatting
across all CLI harnesses built with the cli-anything methodology.
"""
def __init__(self, software: str, version: str = "1.0.0",
history_file: str | None = None, skill_path: str | None = None):
"""Initialize the REPL skin.
Args:
software: Software name (e.g., "gimp", "shotcut", "blender").
version: CLI version string.
history_file: Path for persistent command history.
Defaults to ~/.cli-anything-<software>/history
skill_path: Path to the SKILL.md file for agent discovery.
Auto-detected from the package's skills/ directory if not provided.
Displayed in banner for AI agents to know where to read skill info.
"""
self.software = software.lower().replace("-", "_")
self.display_name = software.replace("_", " ").title()
self.version = version
# Auto-detect skill path from package layout:
# cli_anything/<software>/utils/repl_skin.py (this file)
# cli_anything/<software>/skills/SKILL.md (target)
if skill_path is None:
from pathlib import Path
_auto = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
if _auto.is_file():
skill_path = str(_auto)
self.skill_path = skill_path
self.accent = _ACCENT_COLORS.get(self.software, _DEFAULT_ACCENT)
# History file
if history_file is None:
from pathlib import Path
hist_dir = Path.home() / f".cli-anything-{self.software}"
hist_dir.mkdir(parents=True, exist_ok=True)
self.history_file = str(hist_dir / "history")
else:
self.history_file = history_file
# Detect terminal capabilities
self._color = self._detect_color_support()
def _detect_color_support(self) -> bool:
"""Check if terminal supports color."""
if os.environ.get("NO_COLOR"):
return False
if os.environ.get("CLI_ANYTHING_NO_COLOR"):
return False
if not hasattr(sys.stdout, "isatty"):
return False
return sys.stdout.isatty()
def _c(self, code: str, text: str) -> str:
"""Apply color code if colors are supported."""
if not self._color:
return text
return f"{code}{text}{_RESET}"
# ── Banner ────────────────────────────────────────────────────────
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
pad = inner - _visible_len(content)
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
# Title: ◆ cli-anything · Shotcut
icon = self._c(_CYAN + _BOLD, "")
brand = self._c(_CYAN + _BOLD, "cli-anything")
dot = self._c(_DARK_GRAY, "·")
name = self._c(self.accent + _BOLD, self.display_name)
title = f" {icon} {brand} {dot} {name}"
ver = f" {self._c(_DARK_GRAY, f' v{self.version}')}"
tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}"
empty = ""
# Skill path for agent discovery
skill_line = None
if self.skill_path:
skill_icon = self._c(_MAGENTA, "")
skill_label = self._c(_DARK_GRAY, " Skill:")
skill_path_display = self._c(_LIGHT_GRAY, self.skill_path)
skill_line = f" {skill_icon} {skill_label} {skill_path_display}"
print(top)
print(_box_line(title))
print(_box_line(ver))
if skill_line:
print(_box_line(skill_line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
print()
# ── Prompt ────────────────────────────────────────────────────────
def prompt(self, project_name: str = "", modified: bool = False,
context: str = "") -> str:
"""Build a styled prompt string for prompt_toolkit or input().
Args:
project_name: Current project name (empty if none open).
modified: Whether the project has unsaved changes.
context: Optional extra context to show in prompt.
Returns:
Formatted prompt string.
"""
parts = []
# Icon
if self._color:
parts.append(f"{_CYAN}{_RESET} ")
else:
parts.append("> ")
# Software name
parts.append(self._c(self.accent + _BOLD, self.software))
# Project context
if project_name or context:
ctx = context or project_name
mod = "*" if modified else ""
parts.append(f" {self._c(_DARK_GRAY, '[')}")
parts.append(self._c(_LIGHT_GRAY, f"{ctx}{mod}"))
parts.append(self._c(_DARK_GRAY, ']'))
parts.append(self._c(_GRAY, " "))
return "".join(parts)
def prompt_tokens(self, project_name: str = "", modified: bool = False,
context: str = ""):
"""Build prompt_toolkit formatted text tokens for the prompt.
Use with prompt_toolkit's FormattedText for proper ANSI handling.
Returns:
list of (style, text) tuples for prompt_toolkit.
"""
accent_hex = _ANSI_256_TO_HEX.get(self.accent, "#5fafff")
tokens = []
tokens.append(("class:icon", ""))
tokens.append(("class:software", self.software))
if project_name or context:
ctx = context or project_name
mod = "*" if modified else ""
tokens.append(("class:bracket", " ["))
tokens.append(("class:context", f"{ctx}{mod}"))
tokens.append(("class:bracket", "]"))
tokens.append(("class:arrow", " "))
return tokens
def get_prompt_style(self):
"""Get a prompt_toolkit Style object matching the skin.
Returns:
prompt_toolkit.styles.Style
"""
try:
from prompt_toolkit.styles import Style
except ImportError:
return None
accent_hex = _ANSI_256_TO_HEX.get(self.accent, "#5fafff")
return Style.from_dict({
"icon": "#5fdfdf bold", # cyan brand color
"software": f"{accent_hex} bold",
"bracket": "#585858",
"context": "#bcbcbc",
"arrow": "#808080",
# Completion menu
"completion-menu.completion": "bg:#303030 #bcbcbc",
"completion-menu.completion.current": f"bg:{accent_hex} #000000",
"completion-menu.meta.completion": "bg:#303030 #808080",
"completion-menu.meta.completion.current": f"bg:{accent_hex} #000000",
# Auto-suggest
"auto-suggest": "#585858",
# Bottom toolbar
"bottom-toolbar": "bg:#1c1c1c #808080",
"bottom-toolbar.text": "#808080",
})
# ── Messages ──────────────────────────────────────────────────────
def success(self, message: str):
"""Print a success message with green checkmark."""
icon = self._c(_GREEN + _BOLD, "")
print(f" {icon} {self._c(_GREEN, message)}")
def error(self, message: str):
"""Print an error message with red cross."""
icon = self._c(_RED + _BOLD, "")
print(f" {icon} {self._c(_RED, message)}", file=sys.stderr)
def warning(self, message: str):
"""Print a warning message with yellow triangle."""
icon = self._c(_YELLOW + _BOLD, "")
print(f" {icon} {self._c(_YELLOW, message)}")
def info(self, message: str):
"""Print an info message with blue dot."""
icon = self._c(_BLUE, "")
print(f" {icon} {self._c(_LIGHT_GRAY, message)}")
def hint(self, message: str):
"""Print a subtle hint message."""
print(f" {self._c(_DARK_GRAY, message)}")
def section(self, title: str):
"""Print a section header."""
print()
print(f" {self._c(self.accent + _BOLD, title)}")
print(f" {self._c(_DARK_GRAY, _H_LINE * len(title))}")
# ── Status display ────────────────────────────────────────────────
def status(self, label: str, value: str):
"""Print a key-value status line."""
lbl = self._c(_GRAY, f" {label}:")
val = self._c(_WHITE, f" {value}")
print(f"{lbl}{val}")
def status_block(self, items: dict[str, str], title: str = ""):
"""Print a block of status key-value pairs.
Args:
items: Dict of label -> value pairs.
title: Optional title for the block.
"""
if title:
self.section(title)
max_key = max(len(k) for k in items) if items else 0
for label, value in items.items():
lbl = self._c(_GRAY, f" {label:<{max_key}}")
val = self._c(_WHITE, f" {value}")
print(f"{lbl}{val}")
def progress(self, current: int, total: int, label: str = ""):
"""Print a simple progress indicator.
Args:
current: Current step number.
total: Total number of steps.
label: Optional label for the progress.
"""
pct = int(current / total * 100) if total > 0 else 0
bar_width = 20
filled = int(bar_width * current / total) if total > 0 else 0
bar = "" * filled + "" * (bar_width - filled)
text = f" {self._c(_CYAN, bar)} {self._c(_GRAY, f'{pct:3d}%')}"
if label:
text += f" {self._c(_LIGHT_GRAY, label)}"
print(text)
# ── Table display ─────────────────────────────────────────────────
def table(self, headers: list[str], rows: list[list[str]],
max_col_width: int = 40):
"""Print a formatted table with box-drawing characters.
Args:
headers: Column header strings.
rows: List of rows, each a list of cell strings.
max_col_width: Maximum column width before truncation.
"""
if not headers:
return
# Calculate column widths
col_widths = [min(len(h), max_col_width) for h in headers]
for row in rows:
for i, cell in enumerate(row):
if i < len(col_widths):
col_widths[i] = min(
max(col_widths[i], len(str(cell))), max_col_width
)
def pad(text: str, width: int) -> str:
t = str(text)[:width]
return t + " " * (width - len(t))
# Header
header_cells = [
self._c(_CYAN + _BOLD, pad(h, col_widths[i]))
for i, h in enumerate(headers)
]
sep = self._c(_DARK_GRAY, f" {_V_LINE} ")
header_line = f" {sep.join(header_cells)}"
print(header_line)
# Separator
sep_parts = [self._c(_DARK_GRAY, _H_LINE * w) for w in col_widths]
sep_line = self._c(_DARK_GRAY, f" {'───'.join([_H_LINE * w for w in col_widths])}")
print(sep_line)
# Rows
for row in rows:
cells = []
for i, cell in enumerate(row):
if i < len(col_widths):
cells.append(self._c(_LIGHT_GRAY, pad(str(cell), col_widths[i])))
row_sep = self._c(_DARK_GRAY, f" {_V_LINE} ")
print(f" {row_sep.join(cells)}")
# ── Help display ──────────────────────────────────────────────────
def help(self, commands: dict[str, str]):
"""Print a formatted help listing.
Args:
commands: Dict of command -> description pairs.
"""
self.section("Commands")
max_cmd = max(len(c) for c in commands) if commands else 0
for cmd, desc in commands.items():
cmd_styled = self._c(self.accent, f" {cmd:<{max_cmd}}")
desc_styled = self._c(_GRAY, f" {desc}")
print(f"{cmd_styled}{desc_styled}")
print()
# ── Goodbye ───────────────────────────────────────────────────────
def print_goodbye(self):
"""Print a styled goodbye message."""
print(f"\n {_ICON_SMALL} {self._c(_GRAY, 'Goodbye!')}\n")
# ── Prompt toolkit session factory ────────────────────────────────
def create_prompt_session(self):
"""Create a prompt_toolkit PromptSession with skin styling.
Returns:
A configured PromptSession, or None if prompt_toolkit unavailable.
"""
try:
from prompt_toolkit import PromptSession
from prompt_toolkit.history import FileHistory
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.formatted_text import FormattedText
style = self.get_prompt_style()
session = PromptSession(
history=FileHistory(self.history_file),
auto_suggest=AutoSuggestFromHistory(),
style=style,
enable_history_search=True,
)
return session
except ImportError:
return None
def get_input(self, pt_session, project_name: str = "",
modified: bool = False, context: str = "") -> str:
"""Get input from user using prompt_toolkit or fallback.
Args:
pt_session: A prompt_toolkit PromptSession (or None).
project_name: Current project name.
modified: Whether project has unsaved changes.
context: Optional context string.
Returns:
User input string (stripped).
"""
if pt_session is not None:
from prompt_toolkit.formatted_text import FormattedText
tokens = self.prompt_tokens(project_name, modified, context)
return pt_session.prompt(FormattedText(tokens)).strip()
else:
raw_prompt = self.prompt(project_name, modified, context)
return input(raw_prompt).strip()
# ── Toolbar builder ───────────────────────────────────────────────
def bottom_toolbar(self, items: dict[str, str]):
"""Create a bottom toolbar callback for prompt_toolkit.
Args:
items: Dict of label -> value pairs to show in toolbar.
Returns:
A callable that returns FormattedText for the toolbar.
"""
def toolbar():
from prompt_toolkit.formatted_text import FormattedText
parts = []
for i, (k, v) in enumerate(items.items()):
if i > 0:
parts.append(("class:bottom-toolbar.text", ""))
parts.append(("class:bottom-toolbar.text", f" {k}: "))
parts.append(("class:bottom-toolbar", v))
return FormattedText(parts)
return toolbar
# ── ANSI 256-color to hex mapping (for prompt_toolkit styles) ─────────
_ANSI_256_TO_HEX = {
"\033[38;5;33m": "#0087ff", # audacity navy blue
"\033[38;5;35m": "#00af5f", # shotcut teal
"\033[38;5;39m": "#00afff", # inkscape bright blue
"\033[38;5;40m": "#00d700", # libreoffice green
"\033[38;5;55m": "#5f00af", # obs purple
"\033[38;5;69m": "#5f87ff", # kdenlive slate blue
"\033[38;5;75m": "#5fafff", # default sky blue
"\033[38;5;80m": "#5fd7d7", # brand cyan
"\033[38;5;208m": "#ff8700", # blender deep orange
"\033[38;5;214m": "#ffaf00", # gimp warm orange
}
@@ -1,106 +0,0 @@
#!/usr/bin/env bash
# cli-anything plugin setup script
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Windows bash environment check (helps avoid cryptic cygpath errors later)
is_windows_bash() {
case "$(uname -s 2>/dev/null)" in
CYGWIN*|MINGW*|MSYS*) return 0 ;;
esac
return 1
}
if is_windows_bash && ! command -v cygpath >/dev/null 2>&1; then
echo -e "${RED}${NC} Windows bash environment detected but 'cygpath' was not found."
echo -e "${YELLOW} Please install Git for Windows (Git Bash) or use WSL, then rerun this script.${NC}"
exit 1
fi
# Plugin info
PLUGIN_NAME="cli-anything"
PLUGIN_VERSION="1.0.0"
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${BLUE} cli-anything Plugin v${PLUGIN_VERSION}${NC}"
echo -e "${BLUE} Build powerful CLI interfaces for any GUI application${NC}"
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
# Check if HARNESS.md exists
HARNESS_PATH="/root/cli-anything/HARNESS.md"
if [ ! -f "$HARNESS_PATH" ]; then
echo -e "${YELLOW}⚠️ HARNESS.md not found at $HARNESS_PATH${NC}"
echo -e "${YELLOW} The cli-anything methodology requires HARNESS.md${NC}"
echo -e "${YELLOW} You can create it or specify a custom path with --harness-path${NC}"
echo ""
fi
# Check Python version
if command -v python3 &> /dev/null; then
PYTHON_VERSION=$(python3 --version 2>&1 | awk '{print $2}')
echo -e "${GREEN}${NC} Python 3 detected: ${PYTHON_VERSION}"
else
echo -e "${RED}${NC} Python 3 not found. Please install Python 3.10+"
exit 1
fi
# Check for required Python packages
echo ""
echo "Checking Python dependencies..."
check_package() {
local package=$1
if python3 -c "import $package" 2>/dev/null; then
echo -e "${GREEN}${NC} $package installed"
return 0
else
echo -e "${YELLOW}${NC} $package not installed"
return 1
fi
}
MISSING_PACKAGES=()
check_package "click" || MISSING_PACKAGES+=("click")
check_package "pytest" || MISSING_PACKAGES+=("pytest")
if [ ${#MISSING_PACKAGES[@]} -gt 0 ]; then
echo ""
echo -e "${YELLOW}Missing packages: ${MISSING_PACKAGES[*]}${NC}"
echo -e "${YELLOW}Install with: pip install ${MISSING_PACKAGES[*]}${NC}"
fi
echo ""
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${GREEN} Plugin installed successfully!${NC}"
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
echo "Available commands:"
echo ""
echo -e " ${BLUE}/cli-anything${NC} <path-or-repo> - Build complete CLI harness"
echo -e " ${BLUE}/cli-anything:refine${NC} <path> [focus] - Refine existing harness"
echo -e " ${BLUE}/cli-anything:test${NC} <path-or-repo> - Run tests and update TEST.md"
echo -e " ${BLUE}/cli-anything:validate${NC} <path-or-repo> - Validate against standards"
echo ""
echo "Examples:"
echo ""
echo -e " ${BLUE}/cli-anything${NC} /home/user/gimp"
echo -e " ${BLUE}/cli-anything:refine${NC} /home/user/blender \"particle systems\""
echo -e " ${BLUE}/cli-anything:test${NC} /home/user/inkscape"
echo -e " ${BLUE}/cli-anything:validate${NC} /home/user/audacity"
echo ""
echo "Documentation:"
echo ""
echo " HARNESS.md: /root/cli-anything/HARNESS.md"
echo " Plugin README: Use '/help cli-anything' for more info"
echo ""
echo -e "${GREEN}Ready to build CLI harnesses! 🚀${NC}"
echo ""
@@ -1,527 +0,0 @@
"""
SKILL.md Generator for CLI-Anything
This module extracts metadata from CLI-Anything harnesses and generates
SKILL.md files following the skill-creator methodology.
The generated SKILL.md files contain:
- YAML frontmatter with name and description (triggering metadata)
- Markdown body with usage instructions
- Command documentation
- Examples for AI agents
"""
import re
from pathlib import Path
from typing import Optional
from dataclasses import dataclass, field
def _format_display_name(name: str) -> str:
"""Format software name for display (replace underscores/hyphens with spaces, then title)."""
return name.replace("_", " ").replace("-", " ").title()
@dataclass
class CommandInfo:
"""Information about a CLI command."""
name: str
description: str
@dataclass
class CommandGroup:
"""A group of related CLI commands."""
name: str
description: str
commands: list[CommandInfo] = field(default_factory=list)
@dataclass
class Example:
"""An example of CLI usage."""
title: str
description: str
code: str
@dataclass
class SkillMetadata:
"""Metadata extracted from a CLI-Anything harness."""
skill_name: str
skill_description: str
software_name: str
skill_intro: str
version: str
system_package: Optional[str] = None
command_groups: list[CommandGroup] = field(default_factory=list)
examples: list[Example] = field(default_factory=list)
def extract_cli_metadata(harness_path: str) -> SkillMetadata:
"""
Extract metadata from a CLI-Anything harness directory.
Args:
harness_path: Path to the agent-harness directory
Returns:
SkillMetadata containing extracted information
"""
harness_path = Path(harness_path)
# Find the cli_anything/<software> directory
cli_anything_dir = harness_path / "cli_anything"
if not cli_anything_dir.exists():
raise ValueError(
f"cli_anything directory not found in {harness_path}. "
"Ensure the harness structure includes cli_anything/<software>/"
)
software_dirs = [d for d in cli_anything_dir.iterdir()
if d.is_dir() and (d / "__init__.py").exists()]
if not software_dirs:
raise ValueError(f"No CLI package found in {harness_path}")
software_dir = software_dirs[0]
software_name = software_dir.name
# Extract metadata from README.md
readme_path = software_dir / "README.md"
skill_intro = ""
system_package = None
if readme_path.exists():
readme_content = readme_path.read_text(encoding="utf-8")
skill_intro = extract_intro_from_readme(readme_content)
system_package = extract_system_package(readme_content)
# Extract version from setup.py
setup_path = harness_path / "setup.py"
version = "1.0.0"
if setup_path.exists():
version = extract_version_from_setup(setup_path)
# Extract commands from CLI file
cli_file = software_dir / f"{software_name}_cli.py"
command_groups = []
if cli_file.exists():
command_groups = extract_commands_from_cli(cli_file)
# Generate examples based on software type
examples = generate_examples(software_name, command_groups)
# Build skill name and description
skill_name = f"cli-anything-{software_name}"
skill_description = f"Command-line interface for {_format_display_name(software_name)} - {skill_intro[:100]}..."
return SkillMetadata(
skill_name=skill_name,
skill_description=skill_description,
software_name=software_name,
skill_intro=skill_intro,
version=version,
system_package=system_package,
command_groups=command_groups,
examples=examples
)
def extract_intro_from_readme(content: str) -> str:
"""Extract introduction text from README content."""
# Find the first paragraph after the title
lines = content.split("\n")
intro_lines = []
in_intro = False
for line in lines:
line = line.strip()
if not line:
if in_intro and intro_lines:
break
continue
if line.startswith("# "):
in_intro = True
continue
if line.startswith("##"):
break
if in_intro:
intro_lines.append(line)
return " ".join(intro_lines) or f"CLI interface for the software."
def extract_system_package(content: str) -> Optional[str]:
"""Extract system package installation command from README."""
# Look for apt/brew install patterns
patterns = [
r"`apt install ([\w\-]+)`",
r"`brew install ([\w\-]+)`",
r"apt-get install ([\w\-]+)",
]
for pattern in patterns:
match = re.search(pattern, content)
if match:
package = match.group(1)
if "apt" in pattern:
return f"apt install {package}"
elif "brew" in pattern:
return f"brew install {package}"
return None
def extract_version_from_setup(setup_path: Path) -> str:
"""Extract version from setup.py."""
content = setup_path.read_text(encoding="utf-8")
match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
if match:
return match.group(1)
return "1.0.0"
def extract_commands_from_cli(cli_path: Path) -> list[CommandGroup]:
"""Extract command groups and commands from CLI file."""
content = cli_path.read_text(encoding="utf-8")
groups = []
# Find Click group decorators
# Pattern handles:
# - Multi-line decorators (decorators on separate lines)
# - Docstrings on the same line or following line after function definition
# - Various Click decorator patterns like @click.option(), @click.argument()
# Uses re.DOTALL to match across newlines between decorator and def
group_pattern = (
r'@(\w+)\.group\([^)]*\)' # @xxx.group(...)
r'(?:\s*@[\w.]+\([^)]*\))*' # optional additional decorators
r'\s*def\s+(\w+)\([^)]*\)' # def xxx(...):
r':\s*' # colon with optional whitespace
r'(?:"""([\s\S]*?)"""|\'\'\'([\s\S]*?)\'\'\')?' # optional docstring (""" or ''')
)
for match in re.finditer(group_pattern, content):
group_func = match.group(2)
# Docstring can be in group 3 (triple-double) or group 4 (triple-single)
group_doc = (match.group(3) or match.group(4) or "").strip()
group_name = group_func.replace("_", " ").title()
if not group_name:
group_name = group_func.title()
groups.append(CommandGroup(
name=group_name,
description=group_doc or f"Commands for {group_name.lower()} operations.",
commands=[]
))
# Find Click command decorators
# Pattern handles:
# - Multi-line decorators (decorators on separate lines)
# - Docstrings on the same line or following line after function definition
# - Various Click decorator patterns like @click.option(), @click.argument()
command_pattern = (
r'@(\w+)\.command\([^)]*\)' # @xxx.command(...)
r'(?:\s*@[\w.]+\([^)]*\))*' # optional additional decorators
r'\s*def\s+(\w+)\([^)]*\)' # def xxx(...):
r':\s*' # colon with optional whitespace
r'(?:"""([\s\S]*?)"""|\'\'\'([\s\S]*?)\'\'\')?' # optional docstring (""" or ''')
)
for match in re.finditer(command_pattern, content):
group_name = match.group(1)
cmd_name = match.group(2)
# Docstring can be in group 3 (triple-double) or group 4 (triple-single)
cmd_doc = (match.group(3) or match.group(4) or "").strip()
# Find the matching group
for group in groups:
if group.name.lower().replace(" ", "_") == group_name.lower():
group.commands.append(CommandInfo(
name=cmd_name.replace("_", "-"),
description=cmd_doc or f"Execute {cmd_name} operation."
))
# If no groups found, create a default one with all commands
if not groups:
default_group = CommandGroup(
name="General",
description="General commands for the CLI.",
commands=[]
)
for match in re.finditer(command_pattern, content):
cmd_name = match.group(2)
# Docstring can be in group 3 (triple-double) or group 4 (triple-single)
cmd_doc = (match.group(3) or match.group(4) or "").strip()
default_group.commands.append(CommandInfo(
name=cmd_name.replace("_", "-"),
description=cmd_doc or f"Execute {cmd_name} operation."
))
if default_group.commands:
groups.append(default_group)
return groups
def generate_examples(software_name: str, command_groups: list[CommandGroup]) -> list[Example]:
"""Generate usage examples based on software type and available commands."""
examples = []
# Basic project creation example
examples.append(Example(
title="Create a New Project",
description=f"Create a new {software_name} project file.",
code=f"""cli-anything-{software_name} project new -o myproject.json
# Or with JSON output for programmatic use
cli-anything-{software_name} --json project new -o myproject.json"""
))
# REPL usage example
examples.append(Example(
title="Interactive REPL Session",
description="Start an interactive session with undo/redo support.",
code=f"""cli-anything-{software_name}
# Enter commands interactively
# Use 'help' to see available commands
# Use 'undo' and 'redo' for history navigation"""
))
# Export example if export commands exist
for group in command_groups:
if "export" in group.name.lower():
examples.append(Example(
title="Export Project",
description="Export the project to a final output format.",
code=f"""cli-anything-{software_name} --project myproject.json export render output.pdf --overwrite"""
))
break
return examples
def generate_skill_md(metadata: SkillMetadata, template_path: Optional[str] = None) -> str:
"""
Generate SKILL.md content from metadata using Jinja2 template.
Args:
metadata: SkillMetadata containing CLI information
template_path: Optional path to custom template file
Returns:
Generated SKILL.md content as string
"""
try:
from jinja2 import Environment, FileSystemLoader
except ImportError:
# Fallback to simple string formatting if Jinja2 not available
return generate_skill_md_simple(metadata)
# Load template
if template_path is None:
template_path = Path(__file__).parent / "templates" / "SKILL.md.template"
else:
template_path = Path(template_path)
if not template_path.exists():
return generate_skill_md_simple(metadata)
env = Environment(loader=FileSystemLoader(template_path.parent))
template = env.get_template(template_path.name)
# Render template
return template.render(
skill_name=metadata.skill_name,
skill_description=metadata.skill_description,
software_name=metadata.software_name,
skill_intro=metadata.skill_intro,
version=metadata.version,
system_package=metadata.system_package,
command_groups=[{
"name": g.name,
"description": g.description,
"commands": [{"name": c.name, "description": c.description} for c in g.commands]
} for g in metadata.command_groups],
examples=[{
"title": e.title,
"description": e.description,
"code": e.code
} for e in metadata.examples]
)
def generate_skill_md_simple(metadata: SkillMetadata) -> str:
"""Generate SKILL.md without Jinja2 dependency."""
lines = [
"---",
f'name: "{metadata.skill_name}"',
f'description: "{metadata.skill_description}"',
"---",
"",
f"# {metadata.skill_name}",
"",
metadata.skill_intro,
"",
"## Installation",
"",
f"This CLI is installed as part of the cli-anything-{metadata.software_name} package:",
"",
f"```bash",
f"pip install cli-anything-{metadata.software_name}",
f"```",
"",
"**Prerequisites:**",
"- Python 3.10+",
f"- {_format_display_name(metadata.software_name)} must be installed on your system",
]
if metadata.system_package:
lines.extend([
f"- Install {metadata.software_name}: `{metadata.system_package}`"
])
lines.extend([
"",
"## Usage",
"",
"### Basic Commands",
"",
"```bash",
"# Show help",
f"cli-anything-{metadata.software_name} --help",
"",
"# Start interactive REPL mode",
f"cli-anything-{metadata.software_name}",
"",
"# Create a new project",
f"cli-anything-{metadata.software_name} project new -o project.json",
"",
"# Run with JSON output (for agent consumption)",
f"cli-anything-{metadata.software_name} --json project info -p project.json",
"```",
"",
])
# Add command groups
if metadata.command_groups:
lines.append("## Command Groups")
lines.append("")
for group in metadata.command_groups:
lines.append(f"### {group.name}")
lines.append("")
lines.append(group.description)
lines.append("")
if group.commands:
lines.append("| Command | Description |")
lines.append("|---------|-------------|")
for cmd in group.commands:
lines.append(f"| `{cmd.name}` | {cmd.description} |")
lines.append("")
# Add examples
if metadata.examples:
lines.append("## Examples")
lines.append("")
for example in metadata.examples:
lines.append(f"### {example.title}")
lines.append("")
lines.append(example.description)
lines.append("")
lines.append("```bash")
lines.append(example.code)
lines.append("```")
lines.append("")
# Add AI agent guidance
lines.extend([
"## For AI Agents",
"",
"When using this CLI programmatically:",
"",
"1. **Always use `--json` flag** for parseable output",
"2. **Check return codes** - 0 for success, non-zero for errors",
"3. **Parse stderr** for error messages on failure",
"4. **Use absolute paths** for all file operations",
"5. **Verify outputs exist** after export operations",
"",
"## Version",
"",
metadata.version,
])
return "\n".join(lines)
def generate_skill_file(harness_path: str, output_path: Optional[str] = None,
template_path: Optional[str] = None) -> str:
"""
Generate a SKILL.md file for a CLI-Anything harness.
Args:
harness_path: Path to the agent-harness directory
output_path: Optional output path for SKILL.md (default: cli_anything/<software>/skills/SKILL.md)
template_path: Optional path to custom Jinja2 template
Returns:
Path to the generated SKILL.md file
"""
# Extract metadata
metadata = extract_cli_metadata(harness_path)
# Generate content
content = generate_skill_md(metadata, template_path)
# Determine output path
if output_path is None:
# Default to skills/ directory under harness_path
harness_path_obj = Path(harness_path)
output_path = harness_path_obj / "cli_anything" / metadata.software_name / "skills" / "SKILL.md"
else:
output_path = Path(output_path)
# Ensure output directory exists
output_path.parent.mkdir(parents=True, exist_ok=True)
# Write file
output_path.write_text(content, encoding="utf-8")
return str(output_path)
# CLI interface for standalone usage
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
description="Generate SKILL.md for CLI-Anything harnesses"
)
parser.add_argument(
"harness_path",
help="Path to the agent-harness directory"
)
parser.add_argument(
"-o", "--output",
help="Output path for SKILL.md (default: cli_anything/<software>/skills/SKILL.md)",
default=None
)
parser.add_argument(
"-t", "--template",
help="Path to custom Jinja2 template",
default=None
)
args = parser.parse_args()
output_file = generate_skill_file(
args.harness_path,
args.output,
args.template
)
print(f"Generated: {output_file}")
@@ -1,123 +0,0 @@
---
name: >-
{{ skill_name }}
description: >-
{{ skill_description }}
---
# {{ skill_name }}
{{ skill_intro }}
## Installation
This CLI is installed as part of the cli-anything-{{ software_name }} package:
```bash
pip install cli-anything-{{ software_name }}
```
**Prerequisites:**
- Python 3.10+
- {{ software_name }} must be installed on your system
{% if system_package %}
- Install {{ software_name }}: `{{ system_package }}`
{% endif %}
## Usage
### Basic Commands
```bash
# Show help
cli-anything-{{ software_name }} --help
# Start interactive REPL mode
cli-anything-{{ software_name }}
# Create a new project
cli-anything-{{ software_name }} project new -o project.json
# Run with JSON output (for agent consumption)
cli-anything-{{ software_name }} --json project info -p project.json
```
### REPL Mode
When invoked without a subcommand, the CLI enters an interactive REPL session:
```bash
cli-anything-{{ software_name }}
# Enter commands interactively with tab-completion and history
```
{% if command_groups %}
## Command Groups
{% for group in command_groups %}
### {{ group.name }}
{{ group.description }}
| Command | Description |
|---------|-------------|
{% for cmd in group.commands %}
| `{{ cmd.name }}` | {{ cmd.description }} |
{% endfor %}
{% endfor %}
{% endif %}
## Examples
{% for example in examples %}
### {{ example.title }}
{{ example.description }}
```bash
{{ example.code }}
```
{% endfor %}
## State Management
The CLI maintains session state with:
- **Undo/Redo**: Up to 50 levels of history
- **Project persistence**: Save/load project state as JSON
- **Session tracking**: Track modifications and changes
## Output Formats
All commands support dual output modes:
- **Human-readable** (default): Tables, colors, formatted text
- **Machine-readable** (`--json` flag): Structured JSON for agent consumption
```bash
# Human output
cli-anything-{{ software_name }} project info -p project.json
# JSON output for agents
cli-anything-{{ software_name }} --json project info -p project.json
```
## For AI Agents
When using this CLI programmatically:
1. **Always use `--json` flag** for parseable output
2. **Check return codes** - 0 for success, non-zero for errors
3. **Parse stderr** for error messages on failure
4. **Use absolute paths** for all file operations
5. **Verify outputs exist** after export operations
## More Information
- Full documentation: See README.md in the package
- Test coverage: See TEST.md in the package
- Methodology: See HARNESS.md in the cli-anything-plugin
## Version
{{ version }}