mirror of
https://github.com/HKUDS/CLI-Anything.git
synced 2026-08-30 17:34:27 +08:00
refactor: remove old .pi-extension layout, update .gitignore for new pi-extension dir
This commit is contained in:
+1
-1
@@ -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 }}
|
||||
Reference in New Issue
Block a user