Merge remote-tracking branch 'upstream/main' into codex/unrealinsights-agent-harness

# Conflicts:
#	README.md
This commit is contained in:
aimidi
2026-04-19 02:14:24 +08:00
148 changed files with 18733 additions and 920 deletions
+2 -1
View File
@@ -22,7 +22,8 @@ Fixes #<!-- issue number -->
<!-- If this PR adds a new software CLI inside the monorepo, ALL items below must be checked. -->
- [ ] `<SOFTWARE>.md` SOP document exists at `<software>/agent-harness/<SOFTWARE>.md`
- [ ] `SKILL.md` exists inside the Python package (`cli_anything/<software>/SKILL.md`)
- [ ] Canonical `SKILL.md` exists at `skills/cli-anything-<software>/SKILL.md`
- [ ] Packaged compatibility `SKILL.md` exists at `cli_anything/<software>/skills/SKILL.md`
- [ ] Unit tests at `cli_anything/<software>/tests/test_core.py` are present and pass without backend
- [ ] E2E tests at `cli_anything/<software>/tests/test_full_e2e.py` are present
- [ ] `README.md` includes the new software (with link to harness directory)
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Sync repo-root skills/ from harness-local SKILL.md files."""
from __future__ import annotations
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
ROOT_SKILLS_DIR = REPO_ROOT / "skills"
def _canonical_skill_id(source: Path) -> str:
rel = source.relative_to(REPO_ROOT)
parts = rel.parts
if "cli_anything" in parts:
package_index = parts.index("cli_anything") + 1
if package_index < len(parts):
package_name = parts[package_index]
return f"cli-anything-{package_name.replace('_', '-')}"
software_dir = parts[0]
return f"cli-anything-{software_dir.replace('_', '-')}"
def _rewrite_name_frontmatter(content: str, skill_id: str) -> str:
if not content.startswith("---\n"):
return content
parts = content.split("---\n", 2)
if len(parts) < 3:
return content
_, frontmatter, body = parts
lines = frontmatter.splitlines(keepends=True)
rewritten: list[str] = []
replaced = False
i = 0
while i < len(lines):
line = lines[i]
if not replaced and line.startswith("name:"):
rewritten.append(f'name: "{skill_id}"\n')
replaced = True
i += 1
while i < len(lines) and (lines[i].startswith(" ") or lines[i].startswith("\t")):
i += 1
continue
rewritten.append(line)
i += 1
if not replaced:
rewritten.insert(0, f'name: "{skill_id}"\n')
frontmatter = "".join(rewritten)
return f"---\n{frontmatter}---\n{body}"
def _discover_sources() -> list[Path]:
sources: list[Path] = []
sources.extend(sorted(REPO_ROOT.glob("*/agent-harness/cli_anything/*/skills/SKILL.md")))
sources.extend(sorted(REPO_ROOT.glob("*/agent-harness/cli_anything/*/SKILL.md")))
return [path for path in sources if path.is_file()]
def main() -> int:
sources = _discover_sources()
ROOT_SKILLS_DIR.mkdir(parents=True, exist_ok=True)
for source in sources:
skill_id = _canonical_skill_id(source)
target = ROOT_SKILLS_DIR / skill_id / "SKILL.md"
target.parent.mkdir(parents=True, exist_ok=True)
content = source.read_text(encoding="utf-8")
target.write_text(_rewrite_name_frontmatter(content, skill_id), encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,56 @@
from __future__ import annotations
import importlib.util
from pathlib import Path
SCRIPT_PATH = Path(__file__).resolve().parents[1] / "update_registry_dates.py"
SPEC = importlib.util.spec_from_file_location("update_registry_dates", SCRIPT_PATH)
MODULE = importlib.util.module_from_spec(SPEC)
assert SPEC and SPEC.loader
SPEC.loader.exec_module(MODULE)
def test_resolve_harness_path_prefers_install_subdirectory_for_qgis():
cli = {
"name": "qgis",
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=QGIS/agent-harness",
"skill_md": "QGIS/agent-harness/cli_anything/qgis/skills/SKILL.md",
}
path = MODULE.resolve_harness_path(cli, MODULE.REPO_ROOT)
assert path == MODULE.REPO_ROOT / "QGIS" / "agent-harness"
def test_resolve_harness_path_handles_underscore_directory_names():
cli = {
"name": "unimol_tools",
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=unimol_tools/agent-harness",
"skill_md": "skills/cli-anything-unimol-tools/SKILL.md",
}
path = MODULE.resolve_harness_path(cli, MODULE.REPO_ROOT)
assert path == MODULE.REPO_ROOT / "unimol_tools" / "agent-harness"
def test_extract_external_source_url_from_cargo_git_install():
cli = {
"name": "clibrowser",
"install_cmd": "cargo install --git https://github.com/allthingssecurity/clibrowser.git --tag v0.1.0 --locked",
"source_url": None,
}
source_url = MODULE.extract_external_source_url(cli)
assert source_url == "https://github.com/allthingssecurity/clibrowser"
def test_extract_npm_package_supports_scoped_package_names():
cli = {
"npm_package": "@sentry/cli",
"install_cmd": "npm install -g @sentry/cli",
}
assert MODULE._extract_npm_package(cli) == "@sentry/cli"
+238 -65
View File
@@ -1,105 +1,278 @@
#!/usr/bin/env python3
"""Update registry-dates.json with last modified dates from harness directories."""
"""Update registry-dates.json with meaningful per-CLI update dates."""
from __future__ import annotations
import json
import re
import shlex
import subprocess
import urllib.parse
import urllib.request
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from pathlib import Path
from datetime import datetime
def get_last_modified(harness_path):
"""Get the most recent git commit date for files in a harness directory."""
REPO_ROOT = Path(__file__).resolve().parents[2]
USER_AGENT = "CLI-Anything registry date updater"
GITHUB_REPO_RE = re.compile(r"https://github\.com/([^/]+/[^/#?]+?)(?:\.git)?(?:[/?#].*)?$")
GIT_URL_RE = re.compile(r"https://github\.com/[^\s#]+")
SUBDIRECTORY_RE = re.compile(r"#subdirectory=([^\s]+)")
def _fetch_json(url: str) -> dict | None:
try:
req = urllib.request.Request(
url,
headers={
"Accept": "application/json",
"User-Agent": USER_AGENT,
},
)
with urllib.request.urlopen(req, timeout=10) as resp:
return json.loads(resp.read())
except Exception:
return None
def _fetch_last_modified(url: str) -> str | None:
try:
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}, method="HEAD")
with urllib.request.urlopen(req, timeout=10) as resp:
last_modified = resp.headers.get("Last-Modified")
if not last_modified:
return None
return parsedate_to_datetime(last_modified).astimezone(timezone.utc).strftime("%Y-%m-%d")
except Exception:
return None
def _git_log_timestamp(target_path: Path, excluded_globs: tuple[str, ...] = ()) -> int | None:
try:
relative_target = target_path.relative_to(REPO_ROOT).as_posix()
cmd = ["git", "log", "-1", "--format=%ct", "--", relative_target]
cmd.extend(f":(exclude,glob){pattern}" for pattern in excluded_globs)
result = subprocess.run(
['git', 'log', '-1', '--format=%ct', '--', str(harness_path)],
cmd,
capture_output=True,
text=True,
check=True
check=True,
cwd=REPO_ROOT,
)
timestamp = int(result.stdout.strip())
return datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d')
return int(result.stdout.strip())
except (subprocess.CalledProcessError, ValueError):
return None
def get_github_repo_date(source_url):
def get_last_modified(target_path: Path) -> str | None:
"""Get the most recent git commit date for CLI-specific files in a repo path."""
relative_target = target_path.relative_to(REPO_ROOT).as_posix()
shared_file_globs = (
f"{relative_target}/cli_anything/**/utils/repl_skin.py",
f"{relative_target}/cli_anything/**/skills/SKILL.md",
f"{relative_target}/cli_anything/**/SKILL.md",
)
timestamp = _git_log_timestamp(target_path, excluded_globs=shared_file_globs)
if timestamp is None:
timestamp = _git_log_timestamp(target_path)
if timestamp is None:
return None
try:
return datetime.fromtimestamp(timestamp, tz=timezone.utc).strftime("%Y-%m-%d")
except (OverflowError, OSError, ValueError):
return None
def get_github_repo_date(source_url: str) -> str | None:
"""Get the last push date from a GitHub repo via the API."""
# Extract owner/repo from URL like https://github.com/owner/repo
match = re.match(r'https://github\.com/([^/]+/[^/]+?)(?:\.git)?$', source_url)
match = GITHUB_REPO_RE.match(source_url)
if not match:
return None
repo_slug = match.group(1)
api_url = f'https://api.github.com/repos/{repo_slug}'
try:
req = urllib.request.Request(api_url, headers={'Accept': 'application/vnd.github.v3+json'})
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read())
pushed_at = data.get('pushed_at') # e.g. "2026-04-13T08:00:00Z"
if pushed_at:
return pushed_at[:10]
except Exception:
pass
return None
def get_pypi_date(install_cmd):
"""Get the last release date from PyPI for a simple pip package."""
# Only works for plain "pip install <package>" commands (not git+ URLs)
match = re.match(r'^pip install ([a-zA-Z0-9_-]+)$', install_cmd)
if not match:
data = _fetch_json(f"https://api.github.com/repos/{repo_slug}")
if not data:
return None
pushed_at = data.get("pushed_at")
return pushed_at[:10] if pushed_at else None
def _extract_pypi_package(install_cmd: str) -> str | None:
if not install_cmd:
return None
package = match.group(1)
api_url = f'https://pypi.org/pypi/{package}/json'
try:
with urllib.request.urlopen(api_url, timeout=10) as resp:
data = json.loads(resp.read())
# Get the upload time of the latest version
latest = data.get('info', {}).get('version')
releases = data.get('releases', {})
if latest and releases.get(latest):
upload_time = releases[latest][0].get('upload_time') # e.g. "2026-04-10T12:00:00"
if upload_time:
return upload_time[:10]
except Exception:
pass
tokens = shlex.split(install_cmd)
except ValueError:
return None
if not tokens:
return None
install_index = None
if tokens[:3] == ["python3", "-m", "pip"]:
install_index = 3
elif tokens[0] in {"pip", "pip3"}:
install_index = 1
if install_index is None or install_index >= len(tokens) or tokens[install_index] != "install":
return None
for token in tokens[install_index + 1 :]:
if token.startswith("-"):
continue
if "://" in token or token.startswith("git+"):
return None
return token
return None
def get_external_date(cli):
"""Get the last update date for an external CLI, trying GitHub API then PyPI."""
source_url = cli.get('source_url')
def get_pypi_date(install_cmd: str) -> str | None:
"""Get the last release date from PyPI for a pip-installable package."""
package = _extract_pypi_package(install_cmd)
if not package:
return None
data = _fetch_json(f"https://pypi.org/pypi/{package}/json")
if not data:
return None
latest = data.get("info", {}).get("version")
releases = data.get("releases", {})
release_files = releases.get(latest or "", [])
if not release_files:
return None
upload_time = release_files[0].get("upload_time") or release_files[0].get("upload_time_iso_8601")
return upload_time[:10] if upload_time else None
def _extract_npm_package(cli: dict) -> str | None:
package = cli.get("npm_package")
if package:
return package
install_cmd = cli.get("install_cmd", "")
match = re.search(r"npm install -g (\S+)", install_cmd)
return match.group(1) if match else None
def get_npm_date(cli: dict) -> str | None:
"""Get the latest publish date from the npm registry."""
package = _extract_npm_package(cli)
if not package:
return None
encoded = urllib.parse.quote(package, safe="")
data = _fetch_json(f"https://registry.npmjs.org/{encoded}")
if not data:
return None
latest = data.get("dist-tags", {}).get("latest")
published = data.get("time", {}).get(latest or "")
return published[:10] if published else None
def _extract_install_subdirectory(cli: dict) -> str | None:
install_cmd = cli.get("install_cmd") or ""
match = SUBDIRECTORY_RE.search(install_cmd)
return match.group(1) if match else None
def _extract_skill_subdirectory(cli: dict) -> str | None:
skill_md = cli.get("skill_md")
if not skill_md or skill_md.startswith("http"):
return None
marker = "/agent-harness/"
if marker not in skill_md:
return None
return skill_md.split(marker, 1)[0] + marker.rstrip("/")
def resolve_harness_path(cli: dict, repo_root: Path) -> Path | None:
"""Resolve the on-disk harness path for an in-repo CLI entry."""
for relative in (_extract_install_subdirectory(cli), _extract_skill_subdirectory(cli)):
if relative:
candidate = repo_root / relative
if candidate.exists():
return candidate
candidate_dirs = []
for name in (cli.get("name"), cli.get("name", "").replace("-", "_"), cli.get("name", "").replace("_", "-")):
if name and name not in candidate_dirs:
candidate_dirs.append(name)
for directory in candidate_dirs:
candidate = repo_root / directory / "agent-harness"
if candidate.exists():
return candidate
return None
def extract_external_source_url(cli: dict) -> str | None:
"""Best-effort source URL discovery for third-party CLIs."""
source_url = cli.get("source_url")
if source_url:
return source_url
install_cmd = cli.get("install_cmd") or ""
git_match = GIT_URL_RE.search(install_cmd)
if git_match:
return git_match.group(0).removesuffix(".git")
for field in ("homepage", "docs_url"):
value = cli.get(field)
if value and "github.com/" in value:
return value
return None
def get_external_date(cli: dict) -> str | None:
"""Get a useful update date for external/public CLIs."""
source_url = extract_external_source_url(cli)
if source_url:
date = get_github_repo_date(source_url)
if date:
return date
# Fallback to PyPI
return get_pypi_date(cli.get('install_cmd', ''))
package_manager = (cli.get("package_manager") or "").lower()
if package_manager == "npm":
date = get_npm_date(cli)
if date:
return date
date = get_pypi_date(cli.get("install_cmd", ""))
if date:
return date
for field in ("homepage", "docs_url"):
url = cli.get(field)
if url:
date = _fetch_last_modified(url)
if date:
return date
return None
def main():
repo_root = Path(__file__).parent.parent.parent
registry_path = repo_root / 'registry.json'
dates_path = repo_root / 'docs' / 'hub' / 'registry-dates.json'
def get_cli_date(cli: dict, repo_root: Path) -> str | None:
harness_path = resolve_harness_path(cli, repo_root)
if harness_path:
return get_last_modified(harness_path)
return get_external_date(cli)
with open(registry_path) as f:
data = json.load(f)
dates = {}
for cli in data['clis']:
if cli.get('source_url'):
# External repo: query GitHub API / PyPI for real update date
dates[cli['name']] = get_external_date(cli)
else:
# In-repo: use the last commit in the harness directory
harness_path = repo_root / cli['name'] / 'agent-harness'
if harness_path.exists():
dates[cli['name']] = get_last_modified(harness_path)
def _load_registry(path: Path) -> list[dict]:
with path.open(encoding="utf-8") as f:
return json.load(f)["clis"]
with open(dates_path, 'w') as f:
def main() -> None:
dates_path = REPO_ROOT / "docs" / "hub" / "registry-dates.json"
all_clis = _load_registry(REPO_ROOT / "registry.json") + _load_registry(REPO_ROOT / "public_registry.json")
dates = {cli["name"]: get_cli_date(cli, REPO_ROOT) for cli in all_clis}
with dates_path.open("w", encoding="utf-8") as f:
json.dump(dates, f, indent=2)
print(f"Updated dates for {len(dates)} CLI entries")
if __name__ == '__main__':
if __name__ == "__main__":
main()
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""Validate that deep harness SKILL.md files are mirrored in repo-root skills/."""
from __future__ import annotations
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
def _load_sync_helpers():
namespace: dict[str, object] = {"__file__": str(REPO_ROOT / ".github" / "scripts" / "sync_root_skills.py")}
sync_script = REPO_ROOT / ".github" / "scripts" / "sync_root_skills.py"
exec(sync_script.read_text(encoding="utf-8"), namespace)
return namespace
def main() -> int:
sync = _load_sync_helpers()
discover_sources = sync["_discover_sources"]
canonical_skill_id = sync["_canonical_skill_id"]
rewrite_name_frontmatter = sync["_rewrite_name_frontmatter"]
root_skills_dir = sync["ROOT_SKILLS_DIR"]
errors: list[str] = []
for source in discover_sources():
skill_id = canonical_skill_id(source)
target = root_skills_dir / skill_id / "SKILL.md"
if not target.is_file():
errors.append(
f"Missing root skill for {source.relative_to(REPO_ROOT)}: expected {target.relative_to(REPO_ROOT)}"
)
continue
source_content = source.read_text(encoding="utf-8")
expected = rewrite_name_frontmatter(source_content, skill_id)
actual = target.read_text(encoding="utf-8")
if actual != expected:
errors.append(
f"Out-of-sync root skill for {source.relative_to(REPO_ROOT)}: {target.relative_to(REPO_ROOT)}"
)
if errors:
print("Root skills validation failed:", file=sys.stderr)
for error in errors:
print(f"- {error}", file=sys.stderr)
print(
"Run `python3 .github/scripts/sync_root_skills.py` and commit the updated root skills.",
file=sys.stderr,
)
return 1
print("Root skills validation passed.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+35
View File
@@ -0,0 +1,35 @@
name: Check Root Skills
on:
pull_request:
paths:
- '*/agent-harness/**'
- 'skills/**'
- '.github/scripts/sync_root_skills.py'
- '.github/scripts/validate_root_skills.py'
- '.github/workflows/check-root-skills.yml'
push:
branches:
- main
paths:
- '*/agent-harness/**'
- 'skills/**'
- '.github/scripts/sync_root_skills.py'
- '.github/scripts/validate_root_skills.py'
- '.github/workflows/check-root-skills.yml'
workflow_dispatch:
jobs:
validate-root-skills:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Validate root skills mirror
run: python3 .github/scripts/validate_root_skills.py
+4
View File
@@ -52,6 +52,9 @@
!/openclaw-skill/
!/cli-hub-meta-skill/
!/cli-hub/
!/cli-hub-matrix/
!/skills/
!/skills/**
# Ignore cli-hub-skill (auto-generated, not tracked)
/cli-hub-skill/
@@ -255,6 +258,7 @@ assets/gen_typing_gif.py
# Step 10: Allow CLI Hub registry and frontend
!/registry.json
!/public_registry.json
!/matrix_registry.json
!/docs/
/docs/*
!/docs/hub/
+3 -3
View File
@@ -15,7 +15,7 @@ Adding a new CLI harness is the most impactful contribution. You can either add
Place your code under `<software>/agent-harness/` and ensure the following:
1. **`<SOFTWARE>.md`** — the SOP document exists at `<software>/agent-harness/<SOFTWARE>.md` describing the harness architecture.
2. **`SKILL.md`** — the AI-discoverable skill definition exists inside the Python package at `cli_anything/<software>/SKILL.md`.
2. **`SKILL.md`** — the canonical AI-discoverable skill definition exists at `skills/cli-anything-<software>/SKILL.md`, and the packaged compatibility copy exists at `cli_anything/<software>/skills/SKILL.md`.
3. **Tests** — unit tests (`test_core.py`, passable without backend) and E2E tests (`test_full_e2e.py`) are present and passing.
4. **`README.md`** — the project README includes the new software with a link to its harness directory.
5. **`registry.json`** — add an entry for the new software (see [Registry fields](#registry-fields) below).
@@ -67,7 +67,7 @@ Include an entry in `registry.json` as part of your PR. Each field is described
| `source_url` | Yes | For standalone repos: URL to your repo (e.g. `"https://github.com/user/repo"`). For in-repo harnesses: `null` (the hub auto-links to `<name>/agent-harness/`). |
| `install_cmd` | Yes | Full pip install command. PyPI: `"pip install cli-anything-my-software"`. In-repo: `"pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=my-software/agent-harness"`. |
| `entry_point` | Yes | CLI command name (e.g. `"cli-anything-my-software"`). |
| `skill_md` | Yes | Path to SKILL.md. For standalone repos: full URL (e.g. `"https://github.com/user/repo/blob/main/.../SKILL.md"`). For in-repo: relative path (e.g. `"my-software/agent-harness/cli_anything/my_software/skills/SKILL.md"`). Set to `null` if not yet available. |
| `skill_md` | Yes | Path to canonical SKILL.md. For standalone repos: full URL (e.g. `"https://github.com/user/repo/blob/main/.../SKILL.md"`). For in-repo: relative path under the repo-root `skills/` tree (e.g. `"skills/cli-anything-my-software/SKILL.md"`). Set to `null` if not yet available. |
| `category` | Yes | One of the existing categories (check `registry.json` for examples). |
| `contributors` | Yes | Array of `{"name": "...", "url": "..."}` objects listing all contributors. |
@@ -84,7 +84,7 @@ Include an entry in `registry.json` as part of your PR. Each field is described
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=my-software/agent-harness",
"entry_point": "cli-anything-my-software",
"skill_md": "my-software/agent-harness/cli_anything/my_software/skills/SKILL.md",
"skill_md": "skills/cli-anything-my-software/SKILL.md",
"category": "category-name",
"contributors": [
{"name": "your-github-username", "url": "https://github.com/your-github-username"}
+12 -4
View File
@@ -45,6 +45,12 @@ CLI-Anything: Bridging the Gap Between AI Agents and the World's Software</stron
> Thanks to all invaluable efforts from the community! More updates continuously on the way everyday..
- **2026-04-18** 🧩 **All SKILL.md files are now being unified under the top-level `skills/` directory** — every CLI skill can be installed from one canonical source with `npx skills add HKUDS/CLI-Anything --skill <skill-name> -g -y`. We also added root-skill validation CI, synced contribution / PR docs and REPL skill-path hints to the new layout, and refreshed the **CLI-Hub** install-first frontend around the new `npx skills` flow.
- **2026-04-17** 🌐 **CLI-Hub** received another install UX pass — public registry metadata and skill coverage were tightened, visit counting was corrected, and the web hub was further refined. 🧪 **Shotcut** render output duration was fixed (#92). 📝 **SKILL** contribution paths were corrected for the new docs flow (#224), and the skill generator now safely handles empty intros (#203).
- **2026-04-16** 🗺️ **QGIS CLI** merged (#207) — a full GIS / map authoring harness landed. 🧬 **UniMol Tools CLI** merged (#219) for molecular modeling workflows. 🌐 **CLI-Hub** also added more public CLIs, including **py4csr**, refreshed its generated meta-skill, corrected SKILL contribution docs, and fixed `apt-get` package extraction in skill generation (#204).
- **2026-04-16** 📈 **Unreal Insights CLI** expanded — added background capture session control (`capture start/status/snapshot/stop`), engine-root-matched `UnrealInsights.exe` resolution/build flows, and refreshed docs/tests for the new orchestration workflow.
- **2026-04-15** 🌐 **CLI-Hub** updated to **v0.2.0** — the PyPI package now supports public CLIs from multiple install sources (`pip`, `npm`, `brew`, bundled/system tools), backed by a new `public_registry.json`. The Hub frontend was redesigned with separate **CLI-Anything CLIs** and **Public CLIs** decks, and live end-to-end checks now cover real install, update, and uninstall flows across both pip and npm packages.
@@ -309,6 +315,8 @@ cp CLI-Anything/opencode-commands/*.md .opencode/commands/
cp CLI-Anything/cli-anything-plugin/HARNESS.md .opencode/commands/
```
> **Note:** Please upgrade to the latest OpenCode. Older versions use `command/` (singular) instead of `commands/`. If `commands/` does not exist, use `command/` for both global and project-level installs.
> **Note:** `HARNESS.md` is the methodology spec that all commands reference. It must be in the same directory as the commands.
This adds 5 slash commands: `/cli-anything`, `/cli-anything-refine`, `/cli-anything-test`, `/cli-anything-validate`, and `/cli-anything-list`.
@@ -502,7 +510,7 @@ cli-anything-gimp --json layer add -n "Background" --type solid --color "#1a1a2e
cli-anything-gimp
```
Each installed CLI ships with a [`SKILL.md`](#-skillmd-generation) inside the Python package (`cli_anything/<software>/skills/SKILL.md`). The REPL banner automatically displays the absolute path to this file so AI agents know exactly where to read the skill definition. No extra configuration needed — `pip install` makes the skill discoverable.
Each in-repo harness now has a canonical [`SKILL.md`](#-skillmd-generation) at `skills/cli-anything-<software>/SKILL.md`, which makes the monorepo directly discoverable via `npx skills add HKUDS/CLI-Anything --list`. Installed harness packages still ship a compatibility copy at `cli_anything/<software>/skills/SKILL.md`, and the REPL banner prefers the repo-root canonical file when present, falling back to the packaged copy otherwise.
---
@@ -538,7 +546,7 @@ The agent will browse the catalog, install whichever CLI fits the task, and use
The catalog auto-updates whenever `registry.json` changes — new community CLIs show up automatically.
> **For Claude Code users:** Copy [`cli-hub-meta-skill/SKILL.md`](cli-hub-meta-skill/SKILL.md) into your project or skills directory for the same automatic CLI discovery.
> **For Claude Code users:** Copy [`skills/cli-hub-meta-skill/SKILL.md`](skills/cli-hub-meta-skill/SKILL.md) into your project or skills directory for the same automatic CLI discovery.
---
@@ -671,7 +679,7 @@ All CLIs organized under cli_anything.* namespace — conflict-free, pip-install
### 🤖 SKILL.md Generation
Each generated CLI includes a `SKILL.md` file inside the Python package at `cli_anything/<software>/skills/SKILL.md`. This self-contained skill definition enables AI agents to discover and use the CLI through Claude Code's skill system or other agent frameworks.
Each generated CLI now has a canonical `SKILL.md` at `skills/cli-anything-<software>/SKILL.md`. This makes the current monorepo directly consumable by `npx skills`, while a packaged compatibility copy at `cli_anything/<software>/skills/SKILL.md` preserves installed-harness behavior.
**What SKILL.md provides:**
- **YAML frontmatter** with name and description for agent skill discovery
@@ -679,7 +687,7 @@ Each generated CLI includes a `SKILL.md` file inside the Python package at `cli_
- **Usage examples** for common workflows
- **Agent-specific guidance** for JSON output, error handling, and programmatic use
SKILL.md files are auto-generated during Phase 6.5 of the pipeline using `skill_generator.py`, which extracts metadata directly from the CLI's Click decorators, setup.py, and README. Because the file lives inside the package, it is installed alongside the CLI via `pip install` and auto-detected by the REPL banner agents can read the absolute path displayed at startup.
SKILL.md files are auto-generated during Phase 6.5 of the pipeline using `skill_generator.py`, which extracts metadata directly from the CLI's Click decorators, setup.py, and README. The generator now writes the canonical repo-root skill file and refreshes the package-local compatibility copy used by installed harnesses. Inside this repo, the REPL banner points agents to the canonical root skill path; after `pip install`, it falls back to the packaged copy.
---
@@ -7,7 +7,7 @@ Usage:
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("shotcut", version="1.0.0")
skin.print_banner()
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -57,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -89,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -97,7 +111,7 @@ class ReplSkin:
"""
def __init__(self, software: str, version: str = "1.0.0",
history_file: str | None = None):
history_file: str | None = None, skill_path: str | None = None):
"""Initialize the REPL skin.
Args:
@@ -105,15 +119,45 @@ class ReplSkin:
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 repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
if skill_path is None:
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -143,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -151,6 +197,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -165,9 +229,14 @@ class ReplSkin:
tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}"
empty = ""
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -7,7 +7,7 @@ Usage:
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("shotcut", version="1.0.0")
skin.print_banner()
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -47,7 +48,6 @@ _ACCENT_COLORS = {
"obs_studio": "\033[38;5;55m", # purple
"kdenlive": "\033[38;5;69m", # slate blue
"shotcut": "\033[38;5;35m", # teal green
"anygen": "\033[38;5;141m", # soft violet
}
_DEFAULT_ACCENT = "\033[38;5;75m" # default sky blue
@@ -58,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -90,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -98,7 +111,7 @@ class ReplSkin:
"""
def __init__(self, software: str, version: str = "1.0.0",
history_file: str | None = None):
history_file: str | None = None, skill_path: str | None = None):
"""Initialize the REPL skin.
Args:
@@ -106,15 +119,45 @@ class ReplSkin:
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 repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
if skill_path is None:
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -144,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -152,6 +197,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -166,9 +229,14 @@ class ReplSkin:
tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}"
empty = ""
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -494,7 +562,6 @@ _ANSI_256_TO_HEX = {
"\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;141m": "#af87ff", # anygen soft violet
"\033[38;5;208m": "#ff8700", # blender deep orange
"\033[38;5;214m": "#ffaf00", # gimp warm orange
}
@@ -7,7 +7,7 @@ Usage:
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("shotcut", version="1.0.0")
skin.print_banner()
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -57,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -89,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -97,7 +111,7 @@ class ReplSkin:
"""
def __init__(self, software: str, version: str = "1.0.0",
history_file: str | None = None):
history_file: str | None = None, skill_path: str | None = None):
"""Initialize the REPL skin.
Args:
@@ -105,15 +119,45 @@ class ReplSkin:
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 repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
if skill_path is None:
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -143,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -151,6 +197,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -165,9 +229,14 @@ class ReplSkin:
tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}"
empty = ""
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -7,7 +7,7 @@ Usage:
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("shotcut", version="1.0.0")
skin.print_banner()
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -57,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -89,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -97,7 +111,7 @@ class ReplSkin:
"""
def __init__(self, software: str, version: str = "1.0.0",
history_file: str | None = None):
history_file: str | None = None, skill_path: str | None = None):
"""Initialize the REPL skin.
Args:
@@ -105,15 +119,45 @@ class ReplSkin:
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 repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
if skill_path is None:
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -143,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -151,6 +197,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -165,9 +229,14 @@ class ReplSkin:
tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}"
empty = ""
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -6,20 +6,21 @@ Copy this file into your CLI package at:
Usage:
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("browser", version="1.0.0")
skin.print_banner()
prompt_text = skin.prompt(project_name="https://example.com", modified=False)
skin.success("Page loaded")
skin.error("Connection failed")
skin.warning("DOMShell not found")
skin.info("Navigating...")
skin.status("URL", "https://example.com")
skin = ReplSkin("shotcut", version="1.0.0")
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
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
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -47,8 +48,6 @@ _ACCENT_COLORS = {
"obs_studio": "\033[38;5;55m", # purple
"kdenlive": "\033[38;5;69m", # slate blue
"shotcut": "\033[38;5;35m", # teal green
"ollama": "\033[38;5;255m", # white (Ollama branding)
"browser": "\033[38;5;141m", # lavender (browser harness)
}
_DEFAULT_ACCENT = "\033[38;5;75m" # default sky blue
@@ -59,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -91,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -99,23 +111,53 @@ class ReplSkin:
"""
def __init__(self, software: str, version: str = "1.0.0",
history_file: str | None = None):
history_file: str | None = None, skill_path: str | None = None):
"""Initialize the REPL skin.
Args:
software: Software name (e.g., "gimp", "shotcut", "browser").
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 repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
if skill_path is None:
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -145,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -153,10 +197,28 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
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 · Browser
# Title: ◆ cli-anything · Shotcut
icon = self._c(_CYAN + _BOLD, "")
brand = self._c(_CYAN + _BOLD, "cli-anything")
dot = self._c(_DARK_GRAY, "·")
@@ -167,9 +229,14 @@ class ReplSkin:
tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}"
empty = ""
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -301,7 +368,7 @@ class ReplSkin:
print(f" {self._c(self.accent + _BOLD, title)}")
print(f" {self._c(_DARK_GRAY, _H_LINE * len(title))}")
# ── Status display ───────────────────────────────────────────────
# ── Status display ───────────────────────────────────────────────
def status(self, label: str, value: str):
"""Print a key-value status line."""
@@ -495,8 +562,6 @@ _ANSI_256_TO_HEX = {
"\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;141m": "#afafff", # browser lavender
"\033[38;5;208m": "#ff8700", # blender deep orange
"\033[38;5;214m": "#ffaf00", # gimp warm orange
"\033[38;5;255m": "#eeeeee", # ollama white
}
@@ -6,20 +6,21 @@ Copy this file into your CLI package at:
Usage:
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("ollama", version="1.0.0")
skin.print_banner()
prompt_text = skin.prompt(project_name="llama3.2", modified=False)
skin.success("Model pulled")
skin.error("Connection failed")
skin.warning("No models loaded")
skin.info("Generating...")
skin.status("Model", "llama3.2:latest")
skin = ReplSkin("shotcut", version="1.0.0")
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
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
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -47,7 +48,6 @@ _ACCENT_COLORS = {
"obs_studio": "\033[38;5;55m", # purple
"kdenlive": "\033[38;5;69m", # slate blue
"shotcut": "\033[38;5;35m", # teal green
"ollama": "\033[38;5;255m", # white (Ollama branding)
}
_DEFAULT_ACCENT = "\033[38;5;75m" # default sky blue
@@ -58,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -90,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -98,23 +111,53 @@ class ReplSkin:
"""
def __init__(self, software: str, version: str = "1.0.0",
history_file: str | None = None):
history_file: str | None = None, skill_path: str | None = None):
"""Initialize the REPL skin.
Args:
software: Software name (e.g., "gimp", "shotcut", "ollama").
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 repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
if skill_path is None:
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -144,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -152,10 +197,28 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
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 · Ollama
# Title: ◆ cli-anything · Shotcut
icon = self._c(_CYAN + _BOLD, "")
brand = self._c(_CYAN + _BOLD, "cli-anything")
dot = self._c(_DARK_GRAY, "·")
@@ -166,9 +229,14 @@ class ReplSkin:
tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}"
empty = ""
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -496,5 +564,4 @@ _ANSI_256_TO_HEX = {
"\033[38;5;80m": "#5fd7d7", # brand cyan
"\033[38;5;208m": "#ff8700", # blender deep orange
"\033[38;5;214m": "#ffaf00", # gimp warm orange
"\033[38;5;255m": "#eeeeee", # ollama white
}
+12 -8
View File
@@ -79,7 +79,7 @@ designed for humans, without needing a display or mouse.
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)
skin.print_banner() # Branded startup box (prefers repo-root skills/, falls back to package)
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
@@ -92,8 +92,10 @@ designed for humans, without needing a display or mouse.
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.
- ReplSkin prefers the repo-root canonical `skills/cli-anything-<software>/SKILL.md`
when running inside this monorepo, and falls back to the packaged
`cli_anything/<software>/skills/SKILL.md` copy when installed elsewhere.
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
@@ -257,8 +259,10 @@ automatically, or customize via the Jinja2 template at `templates/SKILL.md.templ
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`.
**Output Location:** The canonical skill lives at
`skills/cli-anything-<software>/SKILL.md`. A compatibility copy is also written to
`cli_anything/<software>/skills/SKILL.md` so installed harnesses still ship a
local skill file.
**Key Principles:**
@@ -270,9 +274,9 @@ process, template customization options, and manual generation commands.
**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.
ReplSkin prefers the repo-root canonical skill path and falls back to the
packaged `skills/SKILL.md` copy. AI agents can read the displayed path to learn
the CLI's full capabilities.
**Package Data:** Ensure `setup.py` includes the skill file so it ships with pip:
+7 -3
View File
@@ -219,7 +219,8 @@ Generate AI-discoverable skill definition:
- Extract CLI metadata using `skill_generator.py`
- Generate SKILL.md with YAML frontmatter (name, description)
- Include command groups, examples, and agent-specific guidance
- Output to `cli_anything/<software>/skills/SKILL.md` (inside the Python package)
- Output canonical skill to `skills/cli-anything-<software>/SKILL.md`
- Refresh package-local compatibility copy at `cli_anything/<software>/skills/SKILL.md`
**Output:** SKILL.md file for AI agent discovery
@@ -237,6 +238,10 @@ Package and install:
## Output Structure
```
skills/
└── cli-anything-<software>/
└── SKILL.md # Canonical repo-root skill for npx skills discovery
<software>/
└── agent-harness/
├── <SOFTWARE>.md # Software-specific SOP
@@ -253,8 +258,6 @@ Package and install:
│ ├── session.py # Undo/redo
│ ├── export.py # Rendering/export
│ └── ... # Domain-specific modules
├── skills/ # AI-discoverable skill definition
│ └── SKILL.md # Installed with the package via package_data
├── utils/ # Utilities
│ ├── __init__.py
│ ├── repl_skin.py # Unified REPL skin (copy from plugin)
@@ -323,6 +326,7 @@ The cli-anything methodology has successfully built CLIs for:
- YAML frontmatter with name and description for triggering
- Command groups and usage examples
- Agent-specific guidance for programmatic usage
- Canonical repo-root `skills/` layout for `npx skills` discovery
- Follows skill-creator methodology
### PyPI Distribution
+9 -3
View File
@@ -73,7 +73,7 @@ This command implements the complete cli-anything methodology to build a product
- 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
- Outputs the canonical skill to `skills/cli-anything-<software>/SKILL.md` and refreshes the packaged compatibility copy at `cli_anything/<software>/skills/SKILL.md`
- Makes the CLI discoverable and usable by AI agents
### Phase 7: PyPI Publishing and Installation
@@ -100,8 +100,6 @@ This command implements the complete cli-anything methodology to build a product
│ ├── session.py
│ ├── export.py
│ └── ...
├── skills/
│ └── SKILL.md # AI-discoverable skill definition
├── utils/ # Utilities
└── tests/
├── TEST.md # Test plan and results
@@ -109,6 +107,14 @@ This command implements the complete cli-anything methodology to build a product
└── test_full_e2e.py # E2E tests
```
Canonical repo-root skill output:
```
skills/
└── cli-anything-<software>/
└── SKILL.md
```
## Example
```bash
+11 -5
View File
@@ -41,7 +41,7 @@ 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
# Default output: skills/cli-anything-<software>/SKILL.md
```
### 2. The generator automatically extracts:
@@ -59,9 +59,14 @@ skill_path = generate_skill_file(
## Output Location
SKILL.md is generated inside the Python package so it is installed with `pip install`:
SKILL.md is generated canonically at the repo root, with a packaged compatibility
copy for installed harnesses:
```
skills/
└── cli-anything-<software>/
└── SKILL.md
<software>/
└── agent-harness/
└── cli_anything/
@@ -93,15 +98,16 @@ skill definition.
## 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:
ReplSkin prefers the repo-root canonical skill file and falls back to the
packaged copy, displaying whichever absolute path is available 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
skin.print_banner() # Displays repo-root skills/cli-anything-<software>/SKILL.md when available
```
## Package Data
+67 -21
View File
@@ -7,7 +7,7 @@ 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
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -57,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -89,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -106,27 +120,44 @@ class ReplSkin:
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.
Auto-detected from the repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Auto-detect skill path from package layout:
# cli_anything/<software>/utils/repl_skin.py (this file)
# cli_anything/<software>/skills/SKILL.md (target)
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
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)
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -156,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -164,6 +197,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -178,19 +229,14 @@ class ReplSkin:
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}"
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
if skill_line:
print(_box_line(skill_line))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
+25 -7
View File
@@ -22,6 +22,14 @@ def _format_display_name(name: str) -> str:
return name.replace("_", " ").replace("-", " ").title()
def _canonical_skill_name(harness_path: Path, software_name: str) -> str:
"""Return the repo-root canonical skill id for a harness."""
software_dir = software_name
if harness_path.name == "agent-harness" and harness_path.parent.name:
software_dir = harness_path.parent.name
return f"cli-anything-{software_dir.replace('_', '-')}"
@dataclass
class CommandInfo:
"""Information about a CLI command."""
@@ -114,8 +122,13 @@ def extract_cli_metadata(harness_path: str) -> SkillMetadata:
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]}..."
skill_name = _canonical_skill_name(harness_path, software_name)
if skill_intro:
intro_snippet = skill_intro[:100]
suffix = "..." if len(skill_intro) > 100 else ""
skill_description = f"Command-line interface for {_format_display_name(software_name)} - {intro_snippet}{suffix}"
else:
skill_description = f"Command-line interface for {_format_display_name(software_name)}"
return SkillMetadata(
skill_name=skill_name,
@@ -467,7 +480,8 @@ def generate_skill_file(harness_path: str, output_path: Optional[str] = None,
Args:
harness_path: Path to the agent-harness directory
output_path: Optional output path for SKILL.md (default: cli_anything/<software>/skills/SKILL.md)
output_path: Optional output path for SKILL.md
(default: skills/cli-anything-<software>/SKILL.md)
template_path: Optional path to custom Jinja2 template
Returns:
@@ -480,10 +494,11 @@ def generate_skill_file(harness_path: str, output_path: Optional[str] = None,
content = generate_skill_md(metadata, template_path)
# Determine output path
harness_path_obj = Path(harness_path)
compatibility_path = harness_path_obj / "cli_anything" / metadata.software_name / "skills" / "SKILL.md"
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"
repo_root = harness_path_obj.parent.parent
output_path = repo_root / "skills" / metadata.skill_name / "SKILL.md"
else:
output_path = Path(output_path)
@@ -492,6 +507,9 @@ def generate_skill_file(harness_path: str, output_path: Optional[str] = None,
# Write file
output_path.write_text(content, encoding="utf-8")
if compatibility_path != output_path:
compatibility_path.parent.mkdir(parents=True, exist_ok=True)
compatibility_path.write_text(content, encoding="utf-8")
return str(output_path)
@@ -509,7 +527,7 @@ if __name__ == "__main__":
)
parser.add_argument(
"-o", "--output",
help="Output path for SKILL.md (default: cli_anything/<software>/skills/SKILL.md)",
help="Output path for SKILL.md (default: skills/cli-anything-<software>/SKILL.md)",
default=None
)
parser.add_argument(
@@ -356,6 +356,9 @@ class TestEdgeCases:
assert metadata.skill_intro == "" # No README → empty intro
assert metadata.version == "1.0.0"
assert metadata.command_groups == []
# skill_description must not contain trailing " - ..." when intro is empty
assert " - " not in metadata.skill_description
assert not metadata.skill_description.endswith("...")
def test_harness_with_system_package(self, tmp_path):
"""README with apt install instructions should extract system_package."""
+2 -3
View File
@@ -40,7 +40,7 @@ SAMPLE_REGISTRY = {
"homepage": "https://gimp.org",
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=gimp/agent-harness",
"entry_point": "cli-anything-gimp",
"skill_md": None,
"skill_md": "skills/cli-anything-gimp/SKILL.md",
"category": "image",
"contributor": "test-user",
"contributor_url": "https://github.com/test-user",
@@ -152,7 +152,6 @@ class TestRegistry:
cats = list_categories()
assert cats == ["3d", "audio", "image"]
# ─── Installer tests ──────────────────────────────────────────────────
@@ -485,7 +484,6 @@ class TestScriptStrategy:
assert data["jimeng"]["strategy"] == "command"
assert data["jimeng"]["package_manager"] == "script"
# ─── Analytics tests ──────────────────────────────────────────────────
@@ -686,6 +684,7 @@ class TestCLI:
result = self.runner.invoke(main, ["info", "gimp"])
assert "GIMP" in result.output
assert "image" in result.output
assert "Install: cli-hub install gimp" in result.output
assert result.exit_code == 0
@patch("cli_hub.cli.track_first_run")
@@ -7,7 +7,7 @@ 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
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -57,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -89,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -106,27 +120,44 @@ class ReplSkin:
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.
Auto-detected from the repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Auto-detect skill path from package layout:
# cli_anything/<software>/utils/repl_skin.py (this file)
# cli_anything/<software>/skills/SKILL.md (target)
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
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)
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -156,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -164,6 +197,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -178,19 +229,14 @@ class ReplSkin:
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}"
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
if skill_line:
print(_box_line(skill_line))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -7,7 +7,7 @@ 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
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -57,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -89,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -106,27 +120,44 @@ class ReplSkin:
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.
Auto-detected from the repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Auto-detect skill path from package layout:
# cli_anything/<software>/utils/repl_skin.py (this file)
# cli_anything/<software>/skills/SKILL.md (target)
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
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)
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -156,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -164,6 +197,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -178,19 +229,14 @@ class ReplSkin:
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}"
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
if skill_line:
print(_box_line(skill_line))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -7,7 +7,7 @@ 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
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -57,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -89,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -106,27 +120,44 @@ class ReplSkin:
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.
Auto-detected from the repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Auto-detect skill path from package layout:
# cli_anything/<software>/utils/repl_skin.py (this file)
# cli_anything/<software>/skills/SKILL.md (target)
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
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)
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -156,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -164,6 +197,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -178,19 +229,14 @@ class ReplSkin:
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}"
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
if skill_line:
print(_box_line(skill_line))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
+166 -56
View File
@@ -258,6 +258,21 @@
flex-shrink: 0;
}
.nav-link-stars {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2.9rem;
padding: 0.14rem 0.52rem;
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.1);
color: var(--text);
font-size: 0.73rem;
font-variant-numeric: tabular-nums;
line-height: 1.1;
}
.theme-toggle {
width: 42px;
padding: 0;
@@ -1334,12 +1349,13 @@
}
.card-command-block {
padding: 0.9rem;
padding: 0.9rem 0.9rem 0.9rem 0.9rem;
border-radius: 18px;
border: 1px solid var(--border);
background:
linear-gradient(180deg, rgba(93, 168, 255, 0.07), rgba(255,255,255,0.02)),
rgba(0,0,0,0.14);
position: relative;
}
.card--public .card-command-block {
@@ -1365,15 +1381,32 @@
font-size: 0.8rem;
color: var(--text);
white-space: pre-wrap;
overflow-wrap: anywhere;
word-break: break-word;
line-height: 1.6;
overflow-wrap: break-word;
word-break: normal;
line-height: 1.45;
padding-top: 0.1rem;
}
.card-command-comment {
display: block;
color: var(--text-tertiary);
font-style: italic;
margin-bottom: 0.24rem;
padding-right: 5.2rem;
}
.card-command-line {
display: block;
}
.card-command-line + .card-command-comment {
margin-top: 0.42rem;
}
.card-command-actions {
display: flex;
justify-content: flex-end;
margin-top: 0.7rem;
position: absolute;
top: 0.96rem;
right: 0.9rem;
}
.card-install-note {
@@ -1485,15 +1518,13 @@
}
.footer-copy a,
.footer-brand a,
.footer-analytics-link {
.footer-brand a {
color: var(--text);
text-decoration: none;
}
.footer-copy a:hover,
.footer-brand a:hover,
.footer-analytics-link:hover {
.footer-brand a:hover {
text-decoration: underline;
}
@@ -1758,12 +1789,13 @@
</span>
</a>
<div class="nav-links">
<a class="nav-link" href="https://reeceyang.sgp1.cdn.digitaloceanspaces.com/SKILL.md" target="_blank">
<a class="nav-link" href="https://github.com/HKUDS/CLI-Anything/blob/main/skills/cli-hub-meta-skill/SKILL.md" target="_blank">
SKILL.md
</a>
<a class="nav-link" href="https://github.com/HKUDS/CLI-Anything" target="_blank">
<svg viewBox="0 0 98 96" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6C29.304 70.25 17.9 66.013 17.9 47.02c0-5.52 1.94-10.046 5.127-13.58-.486-1.302-2.264-6.437.486-13.34 0 0 4.206-1.302 13.59 5.216 3.963-1.14 8.17-1.628 12.34-1.628 4.17 0 8.376.568 12.34 1.628 9.384-6.518 13.59-5.216 13.59-5.216 2.75 6.903.972 12.038.486 13.34 3.268 3.534 5.127 8.06 5.127 13.58 0 19.074-11.485 23.15-22.328 24.29 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"/></svg>
GitHub
<span class="nav-link-stars" data-github-stars aria-label="GitHub star count">--</span>
</a>
<a class="nav-link" href="https://github.com/HKUDS/CLI-Anything/blob/main/CONTRIBUTING.md" target="_blank">Contribute</a>
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle theme">
@@ -1779,7 +1811,7 @@
<h1><em>CLI</em> tools for <span>software, APIs, and autonomous agents.</span></h1>
<p class="hero-lead"><strong>Any software. Any codebase. Any Web API.</strong> Install an orchestration hub that can discover, explain, install, update, and remove CLIs across first-party harnesses and public ecosystems.</p>
<div class="hero-actions">
<a class="btn-primary" href="https://reeceyang.sgp1.cdn.digitaloceanspaces.com/SKILL.md" target="_blank">Get Agent SKILL</a>
<a class="btn-primary" href="https://github.com/HKUDS/CLI-Anything/blob/main/skills/cli-hub-meta-skill/SKILL.md" target="_blank">Get Agent SKILL</a>
<a class="btn-secondary" href="https://github.com/HKUDS/CLI-Anything" target="_blank">
<svg viewBox="0 0 98 96" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6C29.304 70.25 17.9 66.013 17.9 47.02c0-5.52 1.94-10.046 5.127-13.58-.486-1.302-2.264-6.437.486-13.34 0 0 4.206-1.302 13.59 5.216 3.963-1.14 8.17-1.628 12.34-1.628 4.17 0 8.376.568 12.34 1.628 9.384-6.518 13.59-5.216 13.59-5.216 2.75 6.903.972 12.038.486 13.34 3.268 3.534 5.127 8.06 5.127 13.58 0 19.074-11.485 23.15-22.328 24.29 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"/></svg>
Star on GitHub
@@ -1810,6 +1842,11 @@
<section class="rail-panel">
<h3>Empower your agent</h3>
<p>Install the meta-skill once, then let the agent choose the right CLI from the registry.</p>
<div class="install-row">
<span class="chip-label">npx skills</span>
<code>npx skills add HKUDS/CLI-Anything --skill cli-hub-meta-skill -g -y</code>
<button class="copy-btn" onclick="copyCmd(this, 'npx skills add HKUDS/CLI-Anything --skill cli-hub-meta-skill -g -y')">Copy</button>
</div>
<div class="install-row">
<span class="chip-label">OpenClaw</span>
<code>openclaw skills install cli-anything-hub</code>
@@ -1948,13 +1985,17 @@
<span class="control-kicker">Sort mode</span>
</div>
<button class="sort-trigger" id="sort-trigger-public" type="button" aria-haspopup="listbox" aria-expanded="false">
<span class="sort-current" id="sort-current-public">Name</span>
<span class="sort-current" id="sort-current-public">Last updated</span>
<span class="sort-chevron">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
</span>
</button>
<div class="sort-menu" id="sort-menu-public" role="listbox" hidden>
<button class="sort-option" type="button" data-deck="public" data-value="name" role="option" aria-selected="true">
<button class="sort-option" type="button" data-deck="public" data-value="updated" role="option" aria-selected="true">
<span><strong>Last updated</strong><span>Put the freshest public CLIs first.</span></span>
<span class="sort-option-mark"></span>
</button>
<button class="sort-option" type="button" data-deck="public" data-value="name" role="option" aria-selected="false">
<span><strong>Name</strong><span>Alphabetical list for known public tools.</span></span>
<span class="sort-option-mark"></span>
</button>
@@ -1992,7 +2033,7 @@
</div>
<div class="footer-stats">
<div class="footer-label">Traffic Snapshot</div>
<a class="footer-analytics-link" href="https://cloud.umami.is/share/mnilRgKH0oo7SJTs" target="_blank">
<div class="footer-analytics-link" aria-label="Traffic snapshot">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
<span class="stat-dot dot-total"></span>
<span id="stat-total">&mdash;</span> visits
@@ -2002,7 +2043,7 @@
<span class="analytics-sep">&middot;</span>
<span class="stat-dot dot-agent"></span>
<span id="stat-agent">&mdash;</span> AI agent
</a>
</div>
</div>
</footer>
</div>
@@ -2034,6 +2075,8 @@
});
const REPO = 'https://github.com/HKUDS/CLI-Anything';
const GITHUB_REPO_API = 'https://api.github.com/repos/HKUDS/CLI-Anything';
const REPO_SKILLS_SOURCE = 'HKUDS/CLI-Anything';
const REGISTRY_URLS = [
'../../registry.json',
'https://raw.githubusercontent.com/HKUDS/CLI-Anything/main/registry.json'
@@ -2059,7 +2102,7 @@
const deckState = {
harness: { filter: 'all', sort: 'updated', query: '' },
public: { filter: 'all', sort: 'name', query: '' },
public: { filter: 'all', sort: 'updated', query: '' },
};
async function fetchJson(urls) {
@@ -2074,6 +2117,34 @@
return null;
}
function formatCompactCount(value) {
return new Intl.NumberFormat('en', {
notation: 'compact',
maximumFractionDigits: value >= 10000 ? 0 : 1,
}).format(value);
}
async function loadGitHubStars() {
try {
const resp = await fetch(GITHUB_REPO_API, {
headers: { Accept: 'application/vnd.github+json' }
});
if (!resp.ok) return;
const repo = await resp.json();
const stars = repo.stargazers_count;
if (typeof stars !== 'number') return;
document.querySelectorAll('[data-github-stars]').forEach((el) => {
el.textContent = formatCompactCount(stars);
el.setAttribute('aria-label', stars.toLocaleString('en-US') + ' GitHub stars');
el.title = stars.toLocaleString('en-US') + ' GitHub stars';
});
} catch (_) {
// Keep fallback placeholder when GitHub API is unavailable.
}
}
async function loadRegistries() {
try {
const [harnessData, publicData, dates] = await Promise.all([
@@ -2089,6 +2160,7 @@
if (publicData && publicData.clis) {
publicClis = publicData.clis;
if (dates) publicClis.forEach((c) => { c.last_modified = dates[c.name]; });
}
const totalClis = harnessClis.length + publicClis.length;
@@ -2287,6 +2359,45 @@
grid.innerHTML = (deck === 'harness' ? filtered.map(renderHarnessCard) : filtered.map(renderPublicCard)).join('');
}
function repoSkillId(skillPath) {
if (!skillPath || skillPath.startsWith('http')) return '';
const match = skillPath.match(/^skills\/([^/]+)\/SKILL\.md$/);
return match ? match[1] : '';
}
function normalizeNpxSkillsCmd(cmd) {
if (!cmd) return '';
const trimmed = cmd.trim();
if (!trimmed.startsWith('npx skills add ')) return '';
return trimmed
.replace(/\s+-g\b/g, '')
.replace(/\s+-y\b/g, '')
.trim() + ' -g -y';
}
function harnessSkillCmd(c) {
const skillId = repoSkillId(c.skill_md);
return skillId ? 'npx skills add ' + REPO_SKILLS_SOURCE + ' --skill ' + skillId + ' -g -y' : '';
}
function publicSkillCmd(c) {
return normalizeNpxSkillsCmd(c.skill_md);
}
function renderCommandStack(steps, sourceLabel) {
const display = steps.map((step) =>
'<span class="card-command-comment"># ' + esc(step.label.toLowerCase()) + '</span><span class="card-command-line">' + esc(step.cmd) + '</span>'
).join('');
const copyText = steps.map((step) =>
'# ' + step.label.toLowerCase() + '\n' + step.cmd
).join('\n');
return '<div class="card-command-block">' +
'<div class="card-command-label"><span>Install Commands</span><span>' + esc(sourceLabel) + '</span></div>' +
'<div class="card-command">' + display + '</div>' +
'<div class="card-command-actions"><button class="card-copy-btn" onclick=\'copyCmd(this, ' + JSON.stringify(copyText) + ')'>Copy</button></div>' +
'</div>';
}
function renderHarnessCard(c) {
const requiresHtml = c.requires
? '<div class="card-requires"><strong>Requires</strong> ' + esc(c.requires) + '</div>'
@@ -2310,6 +2421,12 @@
: '';
const dateHtml = c.last_modified ? '<div class="card-date">Updated ' + esc(c.last_modified) + '</div>' : '';
const linksHtml = sourceLink + (skillLink ? skillLink : '');
const installSteps = [
{ label: 'step 1 · install cli', cmd: 'cli-hub install ' + c.name, badge: 'CLI-Hub' }
];
const skillCmd = harnessSkillCmd(c);
if (skillCmd) installSteps.push({ label: 'step 2 · install skill', cmd: skillCmd, badge: 'npx skills' });
const installBlock = renderCommandStack(installSteps, 'CLI-Hub');
return `
<article class="card card--harness">
@@ -2326,16 +2443,7 @@
${dateHtml}
${requiresHtml}
</div>
<div class="card-command-block">
<div class="card-command-label">
<span>Install via CLI-Hub</span>
<span>PyPI</span>
</div>
<div class="card-command">pip install cli-anything-hub\ncli-hub install ${esc(c.name)}</div>
<div class="card-command-actions">
<button class="card-copy-btn" onclick="copyCmd(this, 'pip install cli-anything-hub && cli-hub install ${esc(c.name)}')">Copy</button>
</div>
</div>
${installBlock}
<div class="card-footer">
<div class="card-links">${linksHtml}</div>
${contributorHtml}
@@ -2356,6 +2464,7 @@
function renderPublicCard(c) {
const isBundled = (c.install_strategy || c.package_manager) === 'bundled';
const dateHtml = c.last_modified ? '<div class="card-date">Updated ' + esc(c.last_modified) + '</div>' : '';
const requiresHtml = c.requires
? '<div class="card-requires"><strong>Requires</strong> ' + esc(c.requires) + '</div>'
: '';
@@ -2374,13 +2483,14 @@
? '<div class="card-npx">Direct command: <code>' + esc(directCmd) + '</code></div>'
: '';
const installNotesHtml = c.install_notes ? '<div class="card-npx">' + esc(c.install_notes) + '</div>' : '';
const installSteps = isBundled
? []
: [{ label: 'step 1 · install cli', cmd: 'cli-hub install ' + c.name, badge: 'CLI-Hub' }];
const skillCmd = publicSkillCmd(c);
if (skillCmd) installSteps.push({ label: 'step 2 · install skill', cmd: skillCmd, badge: 'npx skills' });
const installBlock = isBundled
? '<div class="card-install-note">Bundled with the upstream application. Install or update the parent app and enable its CLI integration.</div>'
: '<div class="card-command-block">' +
'<div class="card-command-label"><span>Install via CLI-Hub</span><span>Source aware</span></div>' +
'<div class="card-command">pip install cli-anything-hub\ncli-hub install ' + esc(c.name) + '</div>' +
'<div class="card-command-actions"><button class="card-copy-btn" onclick="copyCmd(this, \'pip install cli-anything-hub && cli-hub install ' + esc(c.name) + '\')">Copy</button></div>' +
'</div>';
: renderCommandStack(installSteps, 'Source aware');
return `
<article class="card card--public">
@@ -2394,6 +2504,7 @@
<div class="card-title">${titleHtml}</div>
<div class="card-desc">${esc(c.description)}</div>
<div class="card-submeta">
${dateHtml}
${requiresHtml}
${directCmdHtml}
${installNotesHtml}
@@ -2511,48 +2622,47 @@
const UMAMI_API = 'https://api.umami.is/v1';
const UMAMI_KEY = 'api_idAebMhzn6z0hsUQT7BSxRuCK2GUZvRY';
const WEBSITE_IDS = [
'07082d05-efd3-4f85-a7a1-b426b0e8bfaa',
'a076c661-bed1-405c-a522-813794e688b4',
];
const HKUDS_WEBSITE_ID = '07082d05-efd3-4f85-a7a1-b426b0e8bfaa';
const CC_WEBSITE_ID = 'a076c661-bed1-405c-a522-813794e688b4';
const headers = { Accept: 'application/json', 'x-umami-api-key': UMAMI_KEY };
async function loadVisitorStats() {
try {
const now = Date.now();
let totalVisits = 0;
let humanCount = 0;
const fetches = WEBSITE_IDS.flatMap((id) => [
fetch(UMAMI_API + '/websites/' + id + '/stats?startAt=0&endAt=' + now, { headers }),
fetch(UMAMI_API + '/websites/' + id + '/events/series?startAt=0&endAt=' + now + '&unit=year&timezone=UTC', { headers })
let hkudsHumanVisits = 0;
let ccHumanVisits = 0;
let ccAgentVisits = 0;
const [hkudsStatsResp, ccEventsResp] = await Promise.all([
fetch(UMAMI_API + '/websites/' + HKUDS_WEBSITE_ID + '/stats?startAt=0&endAt=' + now, { headers }),
fetch(UMAMI_API + '/websites/' + CC_WEBSITE_ID + '/events/series?startAt=0&endAt=' + now + '&unit=year&timezone=UTC', { headers })
]);
const [statsResp1, eventsResp1, statsResp2, eventsResp2] = await Promise.all(fetches);
for (const resp of [statsResp1, statsResp2]) {
if (resp.ok) {
const stats = await resp.json();
totalVisits += stats.visits ?? 0;
}
if (hkudsStatsResp.ok) {
const stats = await hkudsStatsResp.json();
hkudsHumanVisits = stats.visits ?? 0;
}
for (const resp of [eventsResp1, eventsResp2]) {
if (resp.ok) {
const events = await resp.json();
events.forEach((e) => {
if (e.x === 'visit-human') humanCount += e.y || 0;
});
}
if (ccEventsResp.ok) {
const events = await ccEventsResp.json();
events.forEach((e) => {
if (e.x === 'visit-human') ccHumanVisits += e.y || 0;
if (e.x === 'visit-agent') ccAgentVisits += e.y || 0;
});
}
const humanCount = hkudsHumanVisits + ccHumanVisits;
const totalVisits = humanCount + ccAgentVisits;
document.getElementById('stat-total').textContent = totalVisits.toLocaleString();
document.getElementById('stat-human').textContent = humanCount.toLocaleString();
document.getElementById('stat-agent').textContent = Math.max(0, totalVisits - humanCount).toLocaleString();
document.getElementById('stat-agent').textContent = ccAgentVisits.toLocaleString();
} catch (_) {
// Leave fallback dashes.
}
}
loadVisitorStats();
loadGitHubStars();
})();
</script>
</body>
+949 -100
View File
File diff suppressed because it is too large Load Diff
@@ -7,7 +7,7 @@ Usage:
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("shotcut", version="1.0.0")
skin.print_banner()
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -47,7 +48,6 @@ _ACCENT_COLORS = {
"obs_studio": "\033[38;5;55m", # purple
"kdenlive": "\033[38;5;69m", # slate blue
"shotcut": "\033[38;5;35m", # teal green
"drawio": "\033[38;5;202m", # draw.io orange
}
_DEFAULT_ACCENT = "\033[38;5;75m" # default sky blue
@@ -58,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -90,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -98,7 +111,7 @@ class ReplSkin:
"""
def __init__(self, software: str, version: str = "1.0.0",
history_file: str | None = None):
history_file: str | None = None, skill_path: str | None = None):
"""Initialize the REPL skin.
Args:
@@ -106,15 +119,45 @@ class ReplSkin:
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 repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
if skill_path is None:
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -144,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -152,6 +197,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -166,9 +229,14 @@ class ReplSkin:
tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}"
empty = ""
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -495,6 +563,5 @@ _ANSI_256_TO_HEX = {
"\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;202m": "#ff5f00", # drawio orange
"\033[38;5;214m": "#ffaf00", # gimp warm orange
}
@@ -7,7 +7,7 @@ Usage:
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("shotcut", version="1.0.0")
skin.print_banner()
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -57,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -89,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -97,7 +111,7 @@ class ReplSkin:
"""
def __init__(self, software: str, version: str = "1.0.0",
history_file: str | None = None):
history_file: str | None = None, skill_path: str | None = None):
"""Initialize the REPL skin.
Args:
@@ -105,15 +119,45 @@ class ReplSkin:
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 repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
if skill_path is None:
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -143,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -151,6 +197,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -165,9 +229,14 @@ class ReplSkin:
tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}"
empty = ""
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -1,3 +1,9 @@
---
name: "cli-anything-exa"
description: >-
Agent-native CLI for Exa web search and content retrieval workflows.
---
# Exa CLI Skill
## Identity
@@ -7,7 +7,7 @@ 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
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -106,21 +106,32 @@ class ReplSkin:
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.
Auto-detected from the repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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)
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
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)
software_aliases = {"iterm2_ctl": "iterm2"}
skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / f"cli-anything-{skill_slug}" / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
self.skill_path = skill_path
self.accent = _ACCENT_COLORS.get(self.software, _DEFAULT_ACCENT)
@@ -7,7 +7,7 @@ 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
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -47,7 +48,6 @@ _ACCENT_COLORS = {
"obs_studio": "\033[38;5;55m", # purple
"kdenlive": "\033[38;5;69m", # slate blue
"shotcut": "\033[38;5;35m", # teal green
"freecad": "\033[38;5;196m", # FreeCAD red
}
_DEFAULT_ACCENT = "\033[38;5;75m" # default sky blue
@@ -58,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -90,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -107,27 +120,44 @@ class ReplSkin:
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.
Auto-detected from the repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Auto-detect skill path from package layout:
# cli_anything/<software>/utils/repl_skin.py (this file)
# cli_anything/<software>/skills/SKILL.md (target)
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
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)
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -157,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -165,6 +197,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -179,19 +229,14 @@ class ReplSkin:
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}"
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
if skill_line:
print(_box_line(skill_line))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -518,6 +563,5 @@ _ANSI_256_TO_HEX = {
"\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;196m": "#ff0000", # freecad red
"\033[38;5;214m": "#ffaf00", # gimp warm orange
}
@@ -7,7 +7,7 @@ Usage:
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("shotcut", version="1.0.0")
skin.print_banner()
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -57,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -89,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -97,7 +111,7 @@ class ReplSkin:
"""
def __init__(self, software: str, version: str = "1.0.0",
history_file: str | None = None):
history_file: str | None = None, skill_path: str | None = None):
"""Initialize the REPL skin.
Args:
@@ -105,15 +119,45 @@ class ReplSkin:
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 repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
if skill_path is None:
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -143,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -151,6 +197,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -165,9 +229,14 @@ class ReplSkin:
tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}"
empty = ""
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -1,3 +1,9 @@
---
name: "cli-anything-godot"
description: >-
Agent-native CLI for Godot project management, scenes, exports, and script execution.
---
# Godot Engine CLI
Agent-native CLI for the Godot game engine. Manage projects, scenes, exports, and GDScript execution from the command line.
@@ -7,7 +7,7 @@ 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
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -57,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -89,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -106,27 +120,44 @@ class ReplSkin:
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.
Auto-detected from the repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Auto-detect skill path from package layout:
# cli_anything/<software>/utils/repl_skin.py (this file)
# cli_anything/<software>/skills/SKILL.md (target)
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
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)
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -156,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -164,6 +197,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -178,19 +229,14 @@ class ReplSkin:
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}"
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
if skill_line:
print(_box_line(skill_line))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -7,7 +7,7 @@ Usage:
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("shotcut", version="1.0.0")
skin.print_banner()
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -57,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -89,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -97,7 +111,7 @@ class ReplSkin:
"""
def __init__(self, software: str, version: str = "1.0.0",
history_file: str | None = None):
history_file: str | None = None, skill_path: str | None = None):
"""Initialize the REPL skin.
Args:
@@ -105,15 +119,45 @@ class ReplSkin:
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 repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
if skill_path is None:
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -143,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -151,6 +197,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -165,9 +229,14 @@ class ReplSkin:
tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}"
empty = ""
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -7,7 +7,7 @@ Usage:
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("shotcut", version="1.0.0")
skin.print_banner()
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -57,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -89,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -97,7 +111,7 @@ class ReplSkin:
"""
def __init__(self, software: str, version: str = "1.0.0",
history_file: str | None = None):
history_file: str | None = None, skill_path: str | None = None):
"""Initialize the REPL skin.
Args:
@@ -105,15 +119,45 @@ class ReplSkin:
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 repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
if skill_path is None:
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -143,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -151,6 +197,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -165,9 +229,14 @@ class ReplSkin:
tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}"
empty = ""
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -7,7 +7,7 @@ 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
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -57,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -89,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -106,27 +120,44 @@ class ReplSkin:
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.
Auto-detected from the repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Auto-detect skill path from package layout:
# cli_anything/<software>/utils/repl_skin.py (this file)
# cli_anything/<software>/skills/SKILL.md (target)
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
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)
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -156,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -164,6 +197,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -178,19 +229,14 @@ class ReplSkin:
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}"
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
if skill_line:
print(_box_line(skill_line))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -7,7 +7,7 @@ Usage:
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("shotcut", version="1.0.0")
skin.print_banner()
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -57,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -89,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -97,7 +111,7 @@ class ReplSkin:
"""
def __init__(self, software: str, version: str = "1.0.0",
history_file: str | None = None):
history_file: str | None = None, skill_path: str | None = None):
"""Initialize the REPL skin.
Args:
@@ -105,15 +119,45 @@ class ReplSkin:
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 repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
if skill_path is None:
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -143,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -151,6 +197,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -165,9 +229,14 @@ class ReplSkin:
tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}"
empty = ""
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -7,7 +7,7 @@ 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
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -47,7 +48,6 @@ _ACCENT_COLORS = {
"obs_studio": "\033[38;5;55m", # purple
"kdenlive": "\033[38;5;69m", # slate blue
"shotcut": "\033[38;5;35m", # teal green
"krita": "\033[38;5;98m", # purple (Krita brand)
}
_DEFAULT_ACCENT = "\033[38;5;75m" # default sky blue
@@ -58,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -90,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -107,27 +120,44 @@ class ReplSkin:
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.
Auto-detected from the repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Auto-detect skill path from package layout:
# cli_anything/<software>/utils/repl_skin.py (this file)
# cli_anything/<software>/skills/SKILL.md (target)
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
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)
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -157,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -165,6 +197,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -179,19 +229,14 @@ class ReplSkin:
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}"
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
if skill_line:
print(_box_line(skill_line))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -516,7 +561,6 @@ _ANSI_256_TO_HEX = {
"\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;98m": "#875fd7", # krita purple
"\033[38;5;80m": "#5fd7d7", # brand cyan
"\033[38;5;208m": "#ff8700", # blender deep orange
"\033[38;5;214m": "#ffaf00", # gimp warm orange
@@ -7,7 +7,7 @@ Usage:
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("shotcut", version="1.0.0")
skin.print_banner()
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -57,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -89,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -97,7 +111,7 @@ class ReplSkin:
"""
def __init__(self, software: str, version: str = "1.0.0",
history_file: str | None = None):
history_file: str | None = None, skill_path: str | None = None):
"""Initialize the REPL skin.
Args:
@@ -105,15 +119,45 @@ class ReplSkin:
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 repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
if skill_path is None:
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -143,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -151,6 +197,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -165,9 +229,14 @@ class ReplSkin:
tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}"
empty = ""
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -2,18 +2,61 @@
from __future__ import annotations
import os
from pathlib import Path
import textwrap
from prompt_toolkit import PromptSession
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.history import InMemoryHistory
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
def _display_home_path(path: str) -> str:
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
def __init__(self, software: str, version: str = "1.0.0"):
def __init__(self, software: str, version: str = "1.0.0", skill_path: str | None = None):
self.software = software
self.version = version
self.skill_slug = self.software.replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
self.skill_path = skill_path or self._detect_skill_path()
def _detect_skill_path(self) -> str | None:
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
return str(candidate)
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
if package_skill.is_file():
return str(package_skill)
return None
def print_banner(self) -> None:
print(f"cli-anything-{self.software} v{self.version}")
install_lines = textwrap.wrap(
self.skill_install_cmd, width=88, break_long_words=True, break_on_hyphens=False
) or [self.skill_install_cmd]
for index, line in enumerate(install_lines):
prefix = "Install: " if index == 0 else " "
print(f"{prefix}{line}")
print(f"Global skill: {_display_home_path(self.global_skill_path)}")
print("Type help for commands, quit to exit")
def create_prompt_session(self) -> PromptSession:
@@ -7,7 +7,7 @@ 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
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -57,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -89,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -106,27 +120,44 @@ class ReplSkin:
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.
Auto-detected from the repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Auto-detect skill path from package layout:
# cli_anything/<software>/utils/repl_skin.py (this file)
# cli_anything/<software>/skills/SKILL.md (target)
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
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)
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -156,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -164,6 +197,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -178,19 +229,14 @@ class ReplSkin:
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}"
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
if skill_line:
print(_box_line(skill_line))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
+13 -2
View File
@@ -237,7 +237,12 @@ def extract_cli_metadata(harness_path: str) -> SkillMetadata:
command_groups = extract_commands_from_cli(cli_file) if cli_file.exists() else []
examples = generate_examples(software_name, command_groups)
skill_name = f"cli-anything-{software_name}"
skill_description = f"Command-line interface for {_format_display_name(software_name)} - {skill_intro[:100]}..."
if skill_intro:
intro_snippet = skill_intro[:100]
suffix = "..." if len(skill_intro) > 100 else ""
skill_description = f"Command-line interface for {_format_display_name(software_name)} - {intro_snippet}{suffix}"
else:
skill_description = f"Command-line interface for {_format_display_name(software_name)}"
return SkillMetadata(
skill_name=skill_name,
@@ -399,12 +404,18 @@ def generate_skill_md(metadata: SkillMetadata, template_path: Optional[str] = No
def generate_skill_file(harness_path: str, output_path: Optional[str] = None, template_path: Optional[str] = None) -> str:
metadata = extract_cli_metadata(harness_path)
content = generate_skill_md(metadata, template_path)
harness_root = Path(harness_path)
skill_id = f"cli-anything-{harness_root.parent.name.replace('_', '-')}"
if output_path is None:
output = Path(harness_path) / "cli_anything" / metadata.software_name / "skills" / "SKILL.md"
output = harness_root.parent.parent / "skills" / skill_id / "SKILL.md"
else:
output = Path(output_path)
mirror = harness_root / "cli_anything" / metadata.software_name / "skills" / "SKILL.md"
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(content, encoding="utf-8")
if mirror != output:
mirror.parent.mkdir(parents=True, exist_ok=True)
mirror.write_text(content, encoding="utf-8")
return str(output)
@@ -7,7 +7,7 @@ Usage:
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("shotcut", version="1.0.0")
skin.print_banner()
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -57,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -89,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -97,7 +111,7 @@ class ReplSkin:
"""
def __init__(self, software: str, version: str = "1.0.0",
history_file: str | None = None):
history_file: str | None = None, skill_path: str | None = None):
"""Initialize the REPL skin.
Args:
@@ -105,15 +119,45 @@ class ReplSkin:
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 repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
if skill_path is None:
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -143,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -151,6 +197,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -165,9 +229,14 @@ class ReplSkin:
tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}"
empty = ""
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -7,7 +7,7 @@ Usage:
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("n8n", version="2.4.7")
skin.print_banner() # auto-detects skills/SKILL.md inside the package
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_workflow", modified=True)
skin.success("Workflow activated")
skin.error("Connection failed")
@@ -22,6 +22,7 @@ import json
import os
import shutil
import sys
from pathlib import Path
from typing import Any
import click
@@ -63,6 +64,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -95,6 +98,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -112,27 +126,43 @@ class ReplSkin:
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.
Auto-detected from the repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
self.skill_slug = self.software.replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Auto-detect skill path from package layout:
# cli_anything/<software>/utils/repl_skin.py (this file)
# cli_anything/<software>/skills/SKILL.md (target)
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
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)
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -162,7 +192,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -170,6 +202,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -184,19 +234,14 @@ class ReplSkin:
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}"
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
if skill_line:
print(_box_line(skill_line))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -7,7 +7,7 @@ Usage:
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("shotcut", version="1.0.0")
skin.print_banner()
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -30,7 +31,7 @@ _ITALIC = "\033[3m"
_UNDERLINE = "\033[4m"
# Brand colors
_CYAN = "\033[38;5;80m" # cli-anything brand cyan
_CYAN = "\033[38;5;80m" # cli-anything brand cyan
_CYAN_BG = "\033[48;5;80m"
_WHITE = "\033[97m"
_GRAY = "\033[38;5;245m"
@@ -39,18 +40,16 @@ _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
"anygen": "\033[38;5;141m", # soft violet
"novita": "\033[38;5;81m", # vivid blue (for Novita AI)
"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
_DEFAULT_ACCENT = "\033[38;5;75m" # default sky blue
# Status colors
_GREEN = "\033[38;5;78m"
@@ -59,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -83,7 +84,6 @@ _CROSS = "┼"
def _strip_ansi(text: str) -> str:
"""Remove ANSI escape codes for length calculation."""
import re
return re.sub(r"\033\[[^m]*m", "", text)
@@ -92,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -99,9 +110,8 @@ class ReplSkin:
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
):
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:
@@ -109,16 +119,45 @@ class ReplSkin:
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 repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
if skill_path is None:
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -148,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -156,10 +197,28 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
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 · Novita
# Title: ◆ cli-anything · Shotcut
icon = self._c(_CYAN + _BOLD, "")
brand = self._c(_CYAN + _BOLD, "cli-anything")
dot = self._c(_DARK_GRAY, "·")
@@ -170,9 +229,14 @@ class ReplSkin:
tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}"
empty = ""
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -180,9 +244,8 @@ class ReplSkin:
# ── Prompt ────────────────────────────────────────────────────────
def prompt(
self, project_name: str = "", modified: bool = False, context: str = ""
) -> str:
def prompt(self, project_name: str = "", modified: bool = False,
context: str = "") -> str:
"""Build a styled prompt string for prompt_toolkit or input().
Args:
@@ -210,15 +273,14 @@ class ReplSkin:
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(_DARK_GRAY, ']'))
parts.append(self._c(_GRAY, " "))
return "".join(parts)
def prompt_tokens(
self, project_name: str = "", modified: bool = False, context: str = ""
):
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.
@@ -256,25 +318,23 @@ class ReplSkin:
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",
}
)
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 ──────────────────────────────────────────────────────
@@ -351,7 +411,8 @@ class ReplSkin:
# ── Table display ─────────────────────────────────────────────────
def table(self, headers: list[str], rows: list[list[str]], max_col_width: int = 40):
def table(self, headers: list[str], rows: list[list[str]],
max_col_width: int = 40):
"""Print a formatted table with box-drawing characters.
Args:
@@ -377,7 +438,8 @@ class ReplSkin:
# Header
header_cells = [
self._c(_CYAN + _BOLD, pad(h, col_widths[i])) for i, h in enumerate(headers)
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)}"
@@ -385,9 +447,7 @@ class ReplSkin:
# 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])}"
)
sep_line = self._c(_DARK_GRAY, f" {'───'.join([_H_LINE * w for w in col_widths])}")
print(sep_line)
# Rows
@@ -447,13 +507,8 @@ class ReplSkin:
except ImportError:
return None
def get_input(
self,
pt_session,
project_name: str = "",
modified: bool = False,
context: str = "",
) -> str:
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:
@@ -467,7 +522,6 @@ class ReplSkin:
"""
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:
@@ -485,10 +539,8 @@ class ReplSkin:
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:
@@ -496,23 +548,20 @@ class ReplSkin:
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;81m": "#5fd7ff", # novita vivid blue
"\033[38;5;141m": "#af87ff", # anygen soft violet
"\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
}
@@ -7,7 +7,7 @@ Usage:
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("shotcut", version="1.0.0")
skin.print_banner()
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -57,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -89,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -97,7 +111,7 @@ class ReplSkin:
"""
def __init__(self, software: str, version: str = "1.0.0",
history_file: str | None = None):
history_file: str | None = None, skill_path: str | None = None):
"""Initialize the REPL skin.
Args:
@@ -105,15 +119,45 @@ class ReplSkin:
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 repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
if skill_path is None:
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -143,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -151,6 +197,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -165,9 +229,14 @@ class ReplSkin:
tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}"
empty = ""
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -6,20 +6,21 @@ Copy this file into your CLI package at:
Usage:
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("ollama", version="1.0.0")
skin.print_banner()
prompt_text = skin.prompt(project_name="llama3.2", modified=False)
skin.success("Model pulled")
skin.error("Connection failed")
skin.warning("No models loaded")
skin.info("Generating...")
skin.status("Model", "llama3.2:latest")
skin = ReplSkin("shotcut", version="1.0.0")
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
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
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -47,7 +48,6 @@ _ACCENT_COLORS = {
"obs_studio": "\033[38;5;55m", # purple
"kdenlive": "\033[38;5;69m", # slate blue
"shotcut": "\033[38;5;35m", # teal green
"ollama": "\033[38;5;255m", # white (Ollama branding)
}
_DEFAULT_ACCENT = "\033[38;5;75m" # default sky blue
@@ -58,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -90,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -98,23 +111,53 @@ class ReplSkin:
"""
def __init__(self, software: str, version: str = "1.0.0",
history_file: str | None = None):
history_file: str | None = None, skill_path: str | None = None):
"""Initialize the REPL skin.
Args:
software: Software name (e.g., "gimp", "shotcut", "ollama").
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 repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
if skill_path is None:
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -144,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -152,10 +197,28 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
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 · Ollama
# Title: ◆ cli-anything · Shotcut
icon = self._c(_CYAN + _BOLD, "")
brand = self._c(_CYAN + _BOLD, "cli-anything")
dot = self._c(_DARK_GRAY, "·")
@@ -166,9 +229,14 @@ class ReplSkin:
tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}"
empty = ""
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -496,5 +564,4 @@ _ANSI_256_TO_HEX = {
"\033[38;5;80m": "#5fd7d7", # brand cyan
"\033[38;5;208m": "#ff8700", # blender deep orange
"\033[38;5;214m": "#ffaf00", # gimp warm orange
"\033[38;5;255m": "#eeeeee", # ollama white
}
@@ -6,20 +6,21 @@ Copy this file into your CLI package at:
Usage:
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("ollama", version="1.0.0")
skin.print_banner()
prompt_text = skin.prompt(project_name="llama3.2", modified=False)
skin.success("Model pulled")
skin.error("Connection failed")
skin.warning("No models loaded")
skin.info("Generating...")
skin.status("Model", "llama3.2:latest")
skin = ReplSkin("shotcut", version="1.0.0")
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
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
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -47,7 +48,6 @@ _ACCENT_COLORS = {
"obs_studio": "\033[38;5;55m", # purple
"kdenlive": "\033[38;5;69m", # slate blue
"shotcut": "\033[38;5;35m", # teal green
"ollama": "\033[38;5;255m", # white (Ollama branding)
}
_DEFAULT_ACCENT = "\033[38;5;75m" # default sky blue
@@ -58,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -90,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -98,23 +111,53 @@ class ReplSkin:
"""
def __init__(self, software: str, version: str = "1.0.0",
history_file: str | None = None):
history_file: str | None = None, skill_path: str | None = None):
"""Initialize the REPL skin.
Args:
software: Software name (e.g., "gimp", "shotcut", "ollama").
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 repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
if skill_path is None:
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -144,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -152,10 +197,28 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
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 · Ollama
# Title: ◆ cli-anything · Shotcut
icon = self._c(_CYAN + _BOLD, "")
brand = self._c(_CYAN + _BOLD, "cli-anything")
dot = self._c(_DARK_GRAY, "·")
@@ -166,9 +229,14 @@ class ReplSkin:
tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}"
empty = ""
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -496,5 +564,4 @@ _ANSI_256_TO_HEX = {
"\033[38;5;80m": "#5fd7d7", # brand cyan
"\033[38;5;208m": "#ff8700", # blender deep orange
"\033[38;5;214m": "#ffaf00", # gimp warm orange
"\033[38;5;255m": "#eeeeee", # ollama white
}
+185
View File
@@ -0,0 +1,185 @@
# OpenClaw Macro System — Agent Harness SOP
## What Is This?
**OpenClaw Macro System** is a layered CLI that turns valuable GUI workflows into
parameterized, agent-callable macros. The agent sends one command:
```bash
cli-anything-openclaw macro run export_png --param output=/tmp/out.png --json
```
The system handles everything else: parameter validation, precondition checks,
backend selection, step execution, postcondition verification, and structured
result output. The agent never touches the GUI directly.
## Architecture
```
Agent
└─▶ cli-anything-openclaw macro run <name> --param k=v --json (L6: CLI)
MacroRuntime (L5)
│ 1. Validate params against MacroDefinition schema
│ 2. Check preconditions (file_exists, process_running, …)
│ 3. For each step:
│ RoutingEngine → select backend by priority (L3)
│ Backend.execute(step, resolved_params) (L2)
│ 4. Check postconditions
│ 5. Collect declared outputs
│ 6. Record telemetry in ExecutionSession
└─▶ { success, output, error, telemetry }
```
## Layer Mapping
| Layer | Name | Implementation |
|-------|------|---------------|
| L7 | Agent Task Interface | Caller (OpenClaw or any agent) |
| L6 | Unified CLI Entry | `openclaw_cli.py` — Click CLI |
| L5 | Macro Execution Runtime | `core/runtime.py` |
| L4 | Parameterized Macro Model | `core/macro_model.py` + `macro_definitions/*.yaml` |
| L3 | Backend Routing Engine | `core/routing.py` |
| L2 | Execution Backends | `backends/` (5 backends) |
| L1 | Target Application | Any GUI-first or closed-source app |
## Execution Backends
| Backend | Priority | Trigger | Use case |
|---------|----------|---------|----------|
| `native_api` | 100 | `backend: native_api` | subprocess / shell commands |
| `gui_macro` | 80 | `backend: gui_macro` | precompiled coordinate replay (pyautogui) |
| `file_transform` | 70 | `backend: file_transform` | XML, JSON, text file editing |
| `semantic_ui` | 50 | `backend: semantic_ui` | accessibility API + keyboard (xdotool) |
| `recovery` | 10 | `backend: recovery` | retry + fallback orchestration |
The RoutingEngine respects the step's explicit `backend:` field; if that backend
is unavailable it walks down the priority list.
## Macro Definition Format
Macros live in `cli_anything/openclaw/macro_definitions/` as YAML files:
```yaml
name: export_png
version: "1.0"
description: Export the active diagram to PNG.
parameters:
output:
type: string
required: true
example: /tmp/diagram.png
preconditions:
- process_running: draw.io
- file_exists: /path/to/input.drawio
steps:
- id: export
backend: native_api
action: run_command
params:
command: [draw.io, --export, --output, "${output}", input.drawio]
timeout_ms: 30000
on_failure: fail # or: skip | continue
postconditions:
- file_exists: ${output}
- file_size_gt:
- ${output}
- 100
outputs:
- name: exported_file
path: ${output}
agent_hints:
danger_level: safe
side_effects: [creates_file]
reversible: true
```
### Supported Condition Types
| Type | Args | Checks |
|------|------|--------|
| `file_exists` | path | `os.path.exists(path)` |
| `file_size_gt` | [path, min_bytes] | `os.stat(path).st_size > min_bytes` |
| `process_running` | name | `pgrep -x name` or psutil |
| `env_var` | name | `name in os.environ` |
| `always` | true/false | constant pass/fail |
## Package Layout
```
openclaw-skill/
└── agent-harness/
├── setup.py entry_point: cli-anything-openclaw
└── cli_anything/openclaw/
├── openclaw_cli.py Main Click CLI
├── macro_definitions/ YAML macro registry
│ ├── manifest.yaml
│ └── examples/
│ ├── export_file.yaml
│ ├── transform_json.yaml
│ └── undo_last.yaml
├── core/
│ ├── macro_model.py MacroDefinition + YAML loader
│ ├── registry.py MacroRegistry
│ ├── routing.py RoutingEngine
│ ├── runtime.py MacroRuntime (full lifecycle)
│ └── session.py ExecutionSession + telemetry
├── backends/
│ ├── base.py Backend ABC + StepResult
│ ├── native_api.py subprocess backend
│ ├── file_transform.py XML/JSON/text backend
│ ├── semantic_ui.py accessibility backend
│ ├── gui_macro.py compiled replay backend
│ └── recovery.py retry/fallback backend
├── skills/SKILL.md Agent-readable skill definition
├── utils/repl_skin.py Unified REPL skin (cli-anything standard)
└── tests/
├── test_core.py Unit tests (49 tests, no external deps)
└── test_full_e2e.py E2E + CLI subprocess tests (15 tests)
```
## Installation
```bash
cd openclaw-skill/agent-harness
pip install -e .
```
**Runtime dependencies:** Python 3.10+, PyYAML, click, prompt-toolkit.
**Optional (for specific backends):**
- `xdotool` — semantic_ui backend on Linux
- `pyautogui` — gui_macro backend
- `psutil` — richer process_running checks
## Running Tests
```bash
cd openclaw-skill/agent-harness
python3 -m pytest cli_anything/openclaw/tests/ -v -s
# 64 passed
```
## Key Design Decisions
**Why YAML macros, not Python?** YAML macros are readable by agents without
running code, inspectable via `macro info`, and editable without touching the
harness source.
**Why 5 backends?** Real GUI applications expose many different control
surfaces. The routing engine picks the most reliable one available — the agent
doesn't need to know which one ran.
**Why preconditions and postconditions?** Agents operate in environments where
state is uncertain. Failing loudly before execution (preconditions) and
verifying after (postconditions) catches problems the agent can act on.
**Why `on_failure: skip | continue`?** Some macro steps are best-effort (e.g.,
confirming a dialog that may or may not appear). Skipping lets the macro
continue to the real work.
@@ -0,0 +1,75 @@
# OpenClaw Macro System
**OpenClaw Macro System** is a layered CLI that converts GUI workflows into
parameterized, agent-callable macros. Agents call `macro run <name>` through
a stable CLI; the system routes execution to the right backend (native plugin,
file transform, semantic UI, or compiled GUI replay) — invisible to the agent.
## Installation
```bash
pip install -e .
```
**Dependencies:** Python 3.10+, PyYAML, click, prompt-toolkit
## Usage
```bash
# List available macros
cli-anything-openclaw macro list --json
# Inspect a macro
cli-anything-openclaw macro info export_file --json
# Execute a macro
cli-anything-openclaw macro run transform_json \
--param file=/tmp/config.json \
--param key=theme --param value=dark --json
# Dry run
cli-anything-openclaw --dry-run macro run export_file \
--param output=/tmp/out.txt --json
# Interactive REPL
cli-anything-openclaw
```
## Run Tests
```bash
cd openclaw-skill/agent-harness
pip install -e ".[dev]"
python -m pytest cli_anything/openclaw/tests/ -v -s
```
## Architecture
```
cli-anything-openclaw (CLI)
└─▶ macro run <name> --param key=value
MacroRuntime
│ validate params
│ check preconditions
│ for each step:
│ RoutingEngine → select backend
│ Backend.execute(step, params)
│ check postconditions
└─▶ ExecutionResult { success, output, telemetry }
```
**Backends:**
- `native_api` — subprocess / shell commands
- `file_transform` — XML, JSON, text file editing
- `semantic_ui` — accessibility controls + keyboard shortcuts
- `gui_macro` — precompiled coordinate-based replay
- `recovery` — retry + fallback orchestration
## Adding a Macro
1. Create `cli_anything/openclaw/macro_definitions/my_macro.yaml`
2. Add it to `macro_definitions/manifest.yaml`
3. Verify: `cli-anything-openclaw macro validate my_macro --json`
See `skills/SKILL.md` (installed with the package) for full macro YAML schema.
@@ -0,0 +1 @@
# cli_anything/openclaw package
@@ -0,0 +1,5 @@
"""Enable: python3 -m cli_anything.openclaw"""
from cli_anything.openclaw.openclaw_cli import cli
if __name__ == "__main__":
cli()
@@ -0,0 +1,89 @@
"""Backend base classes and result types.
All execution backends inherit from Backend and return StepResult.
"""
from __future__ import annotations
import time
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any, Optional
@dataclass
class StepResult:
"""Result of a single macro step execution."""
success: bool
output: dict = field(default_factory=dict)
error: str = ""
duration_ms: float = 0.0
backend_used: str = ""
def to_dict(self) -> dict:
return {
"success": self.success,
"output": self.output,
"error": self.error,
"duration_ms": self.duration_ms,
"backend_used": self.backend_used,
}
class BackendContext:
"""Runtime context passed to each backend during step execution."""
def __init__(
self,
params: dict,
previous_results: Optional[list[StepResult]] = None,
dry_run: bool = False,
timeout_ms: int = 30_000,
):
self.params = params
self.previous_results: list[StepResult] = previous_results or []
self.dry_run = dry_run
self.timeout_ms = timeout_ms
self._start = time.time()
def elapsed_ms(self) -> float:
return (time.time() - self._start) * 1000
class Backend(ABC):
"""Abstract base class for all execution backends.
Concrete backends implement execute() and return a StepResult.
"""
name: str = "base"
priority: int = 0
@abstractmethod
def execute(
self,
step: "MacroStep", # type: ignore[name-defined]
params: dict,
context: BackendContext,
) -> StepResult:
"""Execute a macro step.
Args:
step: The MacroStep definition being executed.
params: Fully resolved (substituted) parameters.
context: Runtime context with previous results and flags.
Returns:
StepResult describing success/failure and captured output.
"""
def is_available(self) -> bool:
"""Return True if this backend can be used in the current environment."""
return True
def describe(self) -> dict:
return {
"name": self.name,
"priority": self.priority,
"available": self.is_available(),
}
@@ -0,0 +1,203 @@
"""FileTransformBackend — read, transform, and write project files.
Supports XML (ElementTree), JSON, and plain text transformations.
Example macro step:
- backend: file_transform
action: json_set
params:
input_file: ${project_file}
output_file: ${project_file}
path: settings.grid_size
value: 20
- backend: file_transform
action: xml_set_attr
params:
input_file: diagram.drawio
output_file: diagram.drawio
xpath: .//mxCell[@id='1']
attr: style
value: rounded=1;
- backend: file_transform
action: text_replace
params:
input_file: config.ini
output_file: config.ini
find: "theme=default"
replace: "theme=dark"
"""
from __future__ import annotations
import json
import os
import time
from pathlib import Path
from cli_anything.openclaw.backends.base import Backend, BackendContext, StepResult
from cli_anything.openclaw.core.macro_model import MacroStep, substitute
class FileTransformBackend(Backend):
"""Transform project files without invoking the target application."""
name = "file_transform"
priority = 70
def execute(self, step: MacroStep, params: dict, context: BackendContext) -> StepResult:
t0 = time.time()
action = step.action
step_params = substitute(step.params, params)
if context.dry_run:
return StepResult(
success=True,
output={"dry_run": True, "action": action},
backend_used=self.name,
duration_ms=(time.time() - t0) * 1000,
)
dispatch = {
"json_get": self._json_get,
"json_set": self._json_set,
"json_delete": self._json_delete,
"xml_set_attr": self._xml_set_attr,
"xml_get_attr": self._xml_get_attr,
"text_replace": self._text_replace,
"copy_file": self._copy_file,
}
handler = dispatch.get(action)
if handler is None:
return StepResult(
success=False,
error=f"FileTransformBackend: unknown action '{action}'.",
backend_used=self.name,
duration_ms=(time.time() - t0) * 1000,
)
try:
output = handler(step_params)
return StepResult(
success=True,
output=output or {},
backend_used=self.name,
duration_ms=(time.time() - t0) * 1000,
)
except Exception as exc:
return StepResult(
success=False,
error=f"FileTransformBackend.{action}: {exc}",
backend_used=self.name,
duration_ms=(time.time() - t0) * 1000,
)
# ── JSON actions ─────────────────────────────────────────────────────
def _json_get(self, p: dict) -> dict:
"""Read a value from a JSON file by dot-path."""
data = self._load_json(p["input_file"])
val = self._dotpath_get(data, p["path"])
return {"value": val, "path": p["path"]}
def _json_set(self, p: dict) -> dict:
"""Set a value in a JSON file by dot-path and write it back."""
path = p.get("path", "")
value = p["value"]
data = self._load_json(p["input_file"]) if Path(p["input_file"]).is_file() else {}
self._dotpath_set(data, path, value)
self._save_json(p.get("output_file", p["input_file"]), data)
return {"path": path, "value": value}
def _json_delete(self, p: dict) -> dict:
"""Delete a key from a JSON file by dot-path."""
data = self._load_json(p["input_file"])
self._dotpath_delete(data, p["path"])
self._save_json(p.get("output_file", p["input_file"]), data)
return {"deleted": p["path"]}
# ── XML actions ──────────────────────────────────────────────────────
def _xml_set_attr(self, p: dict) -> dict:
"""Set an XML element attribute matched by XPath."""
from xml.etree import ElementTree as ET
tree = ET.parse(p["input_file"])
root = tree.getroot()
elements = root.findall(p["xpath"])
if not elements:
raise ValueError(f"XPath matched nothing: {p['xpath']}")
for el in elements:
el.set(p["attr"], str(p["value"]))
tree.write(p.get("output_file", p["input_file"]), encoding="unicode", xml_declaration=True)
return {"matched": len(elements), "attr": p["attr"]}
def _xml_get_attr(self, p: dict) -> dict:
"""Get an XML element attribute matched by XPath."""
from xml.etree import ElementTree as ET
tree = ET.parse(p["input_file"])
root = tree.getroot()
elements = root.findall(p["xpath"])
values = [el.get(p["attr"]) for el in elements]
return {"values": values, "attr": p["attr"]}
# ── Text actions ─────────────────────────────────────────────────────
def _text_replace(self, p: dict) -> dict:
"""Simple find-and-replace in a text file."""
content = Path(p["input_file"]).read_text(encoding="utf-8")
count = content.count(p["find"])
content = content.replace(p["find"], p["replace"])
out = p.get("output_file", p["input_file"])
Path(out).write_text(content, encoding="utf-8")
return {"replacements": count}
def _copy_file(self, p: dict) -> dict:
"""Copy a file from src to dst."""
import shutil
shutil.copy2(p["src"], p["dst"])
size = os.path.getsize(p["dst"])
return {"src": p["src"], "dst": p["dst"], "size": size}
# ── Helpers ──────────────────────────────────────────────────────────
def _load_json(self, path: str) -> dict:
with open(path, encoding="utf-8") as f:
return json.load(f)
def _save_json(self, path: str, data: dict) -> None:
Path(path).parent.mkdir(parents=True, exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
def _dotpath_get(self, data: dict, path: str):
keys = path.split(".")
cur = data
for k in keys:
if isinstance(cur, dict) and k in cur:
cur = cur[k]
else:
return None
return cur
def _dotpath_set(self, data: dict, path: str, value) -> None:
keys = path.split(".")
cur = data
for k in keys[:-1]:
if k not in cur or not isinstance(cur[k], dict):
cur[k] = {}
cur = cur[k]
cur[keys[-1]] = value
def _dotpath_delete(self, data: dict, path: str) -> None:
keys = path.split(".")
cur = data
for k in keys[:-1]:
if isinstance(cur, dict) and k in cur:
cur = cur[k]
else:
return
if isinstance(cur, dict) and keys[-1] in cur:
del cur[keys[-1]]
@@ -0,0 +1,205 @@
"""GUIMacroBackend — replay precompiled coordinate-based macro sequences.
A compiled macro is a JSON blob describing an exact sequence of mouse clicks,
key presses, and wait conditions. These are fast to execute but fragile to
layout changes.
Compiled macro format (stored separately, referenced by step params):
{
"version": 1,
"screen_resolution": "1920x1080",
"layout_hash": "abc123",
"steps": [
{"type": "click", "x": 100, "y": 200, "button": "left", "delay_ms": 200},
{"type": "key", "keys": "ctrl+s", "delay_ms": 100},
{"type": "type", "text": "output.png", "delay_ms": 50},
{"type": "wait_file", "path": "/tmp/out.png", "timeout_ms": 5000},
{"type": "sleep", "ms": 500}
]
}
Example macro step:
- backend: gui_macro
action: replay
params:
macro_file: macros/compiled/export_png.json
layout_strict: false # if true, fail when screen res changes
"""
from __future__ import annotations
import json
import time
from pathlib import Path
from cli_anything.openclaw.backends.base import Backend, BackendContext, StepResult
from cli_anything.openclaw.core.macro_model import MacroStep, substitute
class GUIMacroBackend(Backend):
"""Replay precompiled GUI automation sequences."""
name = "gui_macro"
priority = 80
def execute(self, step: MacroStep, params: dict, context: BackendContext) -> StepResult:
t0 = time.time()
action = step.action
step_params = substitute(step.params, params)
if context.dry_run:
return StepResult(
success=True,
output={"dry_run": True, "action": action},
backend_used=self.name,
duration_ms=(time.time() - t0) * 1000,
)
if action != "replay":
return StepResult(
success=False,
error=f"GUIMacroBackend: unknown action '{action}'. Expected 'replay'.",
backend_used=self.name,
duration_ms=(time.time() - t0) * 1000,
)
return self._replay(step_params, context, t0)
def is_available(self) -> bool:
"""Available when at least one automation library is present."""
for lib in ("pyautogui", "pynput"):
try:
__import__(lib)
return True
except ImportError:
pass
return False
def _replay(self, p: dict, context: BackendContext, t0: float) -> StepResult:
"""Load and replay a compiled macro file."""
macro_file = p.get("macro_file", "")
if not macro_file:
return StepResult(
success=False,
error="GUIMacroBackend.replay: 'macro_file' param is required.",
backend_used=self.name,
duration_ms=(time.time() - t0) * 1000,
)
macro_path = Path(macro_file)
if not macro_path.is_file():
return StepResult(
success=False,
error=f"GUIMacroBackend: compiled macro not found: {macro_file}",
backend_used=self.name,
duration_ms=(time.time() - t0) * 1000,
)
try:
with open(macro_path, encoding="utf-8") as f:
macro_blob = json.load(f)
except Exception as exc:
return StepResult(
success=False,
error=f"GUIMacroBackend: failed to load macro file: {exc}",
backend_used=self.name,
duration_ms=(time.time() - t0) * 1000,
)
layout_strict: bool = p.get("layout_strict", False)
if layout_strict:
check = self._check_layout(macro_blob)
if check:
return StepResult(
success=False,
error=f"GUIMacroBackend: layout mismatch — {check}",
backend_used=self.name,
duration_ms=(time.time() - t0) * 1000,
)
try:
steps_run = self._execute_steps(macro_blob.get("steps", []), context)
return StepResult(
success=True,
output={"steps_executed": steps_run, "macro_file": macro_file},
backend_used=self.name,
duration_ms=(time.time() - t0) * 1000,
)
except Exception as exc:
return StepResult(
success=False,
error=f"GUIMacroBackend.replay: {exc}",
backend_used=self.name,
duration_ms=(time.time() - t0) * 1000,
)
def _check_layout(self, macro_blob: dict) -> str:
"""Return error string if current screen doesn't match expected."""
expected_res = macro_blob.get("screen_resolution", "")
if not expected_res:
return ""
try:
import pyautogui
w, h = pyautogui.size()
current_res = f"{w}x{h}"
if current_res != expected_res:
return f"screen is {current_res}, macro expects {expected_res}"
except ImportError:
pass # Can't verify — allow through
return ""
def _execute_steps(self, steps: list, context: BackendContext) -> int:
"""Execute each step in the compiled macro."""
try:
import pyautogui
has_pyautogui = True
except ImportError:
has_pyautogui = False
count = 0
for s in steps:
stype = s.get("type", "")
delay = s.get("delay_ms", 100) / 1000.0
if stype == "click":
if not has_pyautogui:
raise ImportError("pyautogui required for click steps. pip install pyautogui")
button = s.get("button", "left")
pyautogui.click(s["x"], s["y"], button=button)
elif stype == "key":
if not has_pyautogui:
raise ImportError("pyautogui required for key steps. pip install pyautogui")
keys = s.get("keys", "").split("+")
if len(keys) == 1:
pyautogui.press(keys[0])
else:
pyautogui.hotkey(*keys)
elif stype == "type":
if not has_pyautogui:
raise ImportError("pyautogui required for type steps. pip install pyautogui")
pyautogui.typewrite(s.get("text", ""), interval=0.03)
elif stype == "wait_file":
deadline = time.time() + s.get("timeout_ms", 5000) / 1000.0
path = s.get("path", "")
while time.time() < deadline:
if Path(path).exists():
break
time.sleep(0.1)
else:
raise TimeoutError(f"wait_file timed out: {path}")
delay = 0 # no additional delay after file wait
elif stype == "sleep":
time.sleep(s.get("ms", 500) / 1000.0)
delay = 0
if delay > 0:
time.sleep(delay)
count += 1
return count
@@ -0,0 +1,167 @@
"""NativeAPIBackend — executes macro steps via subprocess.
Supports these action types (configured in macro step params):
action: run_command
params:
command: [inkscape, --export-filename, /tmp/out.png, input.svg]
cwd: /optional/working/dir # optional
env: {KEY: value} # optional extra env vars
capture_stdout: true # store stdout in output.stdout
action: find_executable
params:
name: inkscape
candidates: [inkscape, inkscape-1.0, /usr/bin/inkscape]
install_hint: "apt install inkscape"
"""
from __future__ import annotations
import os
import shutil
import subprocess
import time
from typing import Any
from cli_anything.openclaw.backends.base import Backend, BackendContext, StepResult
from cli_anything.openclaw.core.macro_model import MacroStep, substitute
class NativeAPIBackend(Backend):
"""Execute a macro step by running an external command."""
name = "native_api"
priority = 100
def execute(self, step: MacroStep, params: dict, context: BackendContext) -> StepResult:
t0 = time.time()
action = step.action
if action == "find_executable":
return self._find_executable(step, params, context, t0)
elif action == "run_command":
return self._run_command(step, params, context, t0)
else:
return StepResult(
success=False,
error=f"NativeAPIBackend: unknown action '{action}'.",
backend_used=self.name,
duration_ms=(time.time() - t0) * 1000,
)
# ── Actions ──────────────────────────────────────────────────────────
def _find_executable(
self, step: MacroStep, params: dict, context: BackendContext, t0: float
) -> StepResult:
"""Check that an executable exists; return its path."""
step_params = substitute(step.params, params)
exe_name = step_params.get("name", "")
candidates: list[str] = step_params.get("candidates", [exe_name] if exe_name else [])
install_hint: str = step_params.get("install_hint", f"Install {exe_name}")
for candidate in candidates:
found = shutil.which(candidate)
if found:
return StepResult(
success=True,
output={"executable": found, "name": candidate},
backend_used=self.name,
duration_ms=(time.time() - t0) * 1000,
)
return StepResult(
success=False,
error=(
f"Executable not found: {exe_name}. "
f"Tried: {candidates}. "
f"Install with: {install_hint}"
),
backend_used=self.name,
duration_ms=(time.time() - t0) * 1000,
)
def _run_command(
self, step: MacroStep, params: dict, context: BackendContext, t0: float
) -> StepResult:
"""Run an external command."""
step_params = substitute(step.params, params)
command: list[str] = step_params.get("command", [])
if not command:
return StepResult(
success=False,
error="NativeAPIBackend.run_command: 'command' param is required.",
backend_used=self.name,
duration_ms=(time.time() - t0) * 1000,
)
if isinstance(command, str):
import shlex
command = shlex.split(command)
command = [str(c) for c in command]
cwd: str = step_params.get("cwd", "")
extra_env: dict = step_params.get("env", {})
capture_stdout: bool = step_params.get("capture_stdout", False)
env = os.environ.copy()
if extra_env:
env.update({k: str(v) for k, v in extra_env.items()})
timeout_s = context.timeout_ms / 1000.0
if context.dry_run:
return StepResult(
success=True,
output={"dry_run": True, "command": command},
backend_used=self.name,
duration_ms=(time.time() - t0) * 1000,
)
try:
result = subprocess.run(
command,
capture_output=True,
text=True,
timeout=timeout_s,
cwd=cwd or None,
env=env,
)
except FileNotFoundError as exc:
return StepResult(
success=False,
error=f"Command not found: {command[0]}. {exc}",
backend_used=self.name,
duration_ms=(time.time() - t0) * 1000,
)
except subprocess.TimeoutExpired:
return StepResult(
success=False,
error=f"Command timed out after {timeout_s:.0f}s: {command}",
backend_used=self.name,
duration_ms=(time.time() - t0) * 1000,
)
duration = (time.time() - t0) * 1000
if result.returncode != 0:
return StepResult(
success=False,
error=(
f"Command failed (exit {result.returncode}): {command}\n"
f"stderr: {result.stderr.strip()}"
),
output={"returncode": result.returncode, "stderr": result.stderr},
backend_used=self.name,
duration_ms=duration,
)
output: dict[str, Any] = {"returncode": 0}
if capture_stdout:
output["stdout"] = result.stdout
return StepResult(
success=True,
output=output,
backend_used=self.name,
duration_ms=duration,
)
@@ -0,0 +1,128 @@
"""RecoveryBackend — retry and fallback orchestration.
This backend wraps another backend and retries failed steps with exponential
backoff. It can also fall back to an alternative backend on exhausted retries.
Example macro step using recovery explicitly:
- backend: recovery
action: retry_with_fallback
params:
primary_backend: native_api
fallback_backend: file_transform
max_retries: 3
backoff_ms: [1000, 2000, 5000]
step:
action: run_command
params:
command: [inkscape, --export-filename, ${output}, input.svg]
The MacroRuntime also uses the RecoveryBackend automatically when a step
specifies retry_max > 0 in the macro definition.
"""
from __future__ import annotations
import time
from cli_anything.openclaw.backends.base import Backend, BackendContext, StepResult
from cli_anything.openclaw.core.macro_model import MacroStep, substitute
class RecoveryBackend(Backend):
"""Retry and fallback orchestration backend."""
name = "recovery"
priority = 10 # lowest — last resort
def __init__(self, backends: dict[str, "Backend"] | None = None):
"""
Args:
backends: Dict of backend_name -> Backend instance.
Injected by the RoutingEngine at runtime.
"""
self._backends = backends or {}
def register_backend(self, backend: "Backend") -> None:
self._backends[backend.name] = backend
def execute(self, step: MacroStep, params: dict, context: BackendContext) -> StepResult:
t0 = time.time()
action = step.action
if action not in ("retry", "retry_with_fallback"):
return StepResult(
success=False,
error=f"RecoveryBackend: unknown action '{action}'. "
"Use 'retry' or 'retry_with_fallback'.",
backend_used=self.name,
duration_ms=(time.time() - t0) * 1000,
)
step_params = substitute(step.params, params)
inner_step_raw = step_params.get("step", {})
if not inner_step_raw:
return StepResult(
success=False,
error="RecoveryBackend: 'step' param is required.",
backend_used=self.name,
duration_ms=(time.time() - t0) * 1000,
)
# Build an inner MacroStep from the nested definition
inner_step = MacroStep(
id=inner_step_raw.get("id", "recovery_inner"),
backend=step_params.get("primary_backend", inner_step_raw.get("backend", "native_api")),
action=inner_step_raw.get("action", ""),
params=inner_step_raw.get("params", {}),
timeout_ms=context.timeout_ms,
)
max_retries: int = int(step_params.get("max_retries", step.retry_max or 2))
backoff_ms: list[int] = step_params.get("backoff_ms", [1000, 2000, 5000])
fallback_name: str = step_params.get("fallback_backend", "")
last_result = None
for attempt in range(max_retries + 1):
backend = self._backends.get(inner_step.backend)
if backend is None:
return StepResult(
success=False,
error=f"RecoveryBackend: backend '{inner_step.backend}' not registered.",
backend_used=self.name,
duration_ms=(time.time() - t0) * 1000,
)
last_result = backend.execute(inner_step, params, context)
if last_result.success:
last_result.backend_used = f"{self.name}({inner_step.backend}, attempt={attempt + 1})"
return last_result
# Failed — decide whether to retry or fall back
if attempt < max_retries:
wait = backoff_ms[min(attempt, len(backoff_ms) - 1)] / 1000.0
time.sleep(wait)
elif fallback_name and fallback_name in self._backends:
# Switch to fallback backend for one final attempt
inner_step = MacroStep(
id=inner_step.id,
backend=fallback_name,
action=inner_step.action,
params=inner_step.params,
timeout_ms=inner_step.timeout_ms,
)
fallback_result = self._backends[fallback_name].execute(inner_step, params, context)
if fallback_result.success:
fallback_result.backend_used = f"{self.name}(fallback={fallback_name})"
return fallback_result
last_result = fallback_result
if last_result is None:
last_result = StepResult(
success=False,
error="RecoveryBackend: no attempts made.",
backend_used=self.name,
)
last_result.duration_ms = (time.time() - t0) * 1000
last_result.backend_used = self.name
return last_result
@@ -0,0 +1,202 @@
"""SemanticUIBackend — drive applications via accessibility and keyboard shortcuts.
This backend provides stubs for AT-SPI (Linux), Windows UIA, and macOS
accessibility APIs. Full implementations require platform-specific libraries
(pyatspi2, pywinauto, pyobjc-framework-Accessibility).
Example macro step:
- backend: semantic_ui
action: menu_click
params:
menu_path: [File, Export As, PNG]
- backend: semantic_ui
action: shortcut
params:
keys: ctrl+shift+e
- backend: semantic_ui
action: wait_for_window
params:
title_contains: Export
timeout_ms: 5000
"""
from __future__ import annotations
import platform
import time
from cli_anything.openclaw.backends.base import Backend, BackendContext, StepResult
from cli_anything.openclaw.core.macro_model import MacroStep, substitute
class SemanticUIBackend(Backend):
"""Drive applications through semantic (accessibility) controls."""
name = "semantic_ui"
priority = 50
def execute(self, step: MacroStep, params: dict, context: BackendContext) -> StepResult:
t0 = time.time()
action = step.action
step_params = substitute(step.params, params)
if context.dry_run:
return StepResult(
success=True,
output={"dry_run": True, "action": action},
backend_used=self.name,
duration_ms=(time.time() - t0) * 1000,
)
dispatch = {
"shortcut": self._shortcut,
"menu_click": self._menu_click,
"wait_for_window": self._wait_for_window,
"button_click": self._button_click,
"type_text": self._type_text,
}
handler = dispatch.get(action)
if handler is None:
return StepResult(
success=False,
error=f"SemanticUIBackend: unknown action '{action}'.",
backend_used=self.name,
duration_ms=(time.time() - t0) * 1000,
)
try:
output = handler(step_params)
return StepResult(
success=True,
output=output or {},
backend_used=self.name,
duration_ms=(time.time() - t0) * 1000,
)
except NotImplementedError as exc:
return StepResult(
success=False,
error=str(exc),
backend_used=self.name,
duration_ms=(time.time() - t0) * 1000,
)
except Exception as exc:
return StepResult(
success=False,
error=f"SemanticUIBackend.{action}: {exc}",
backend_used=self.name,
duration_ms=(time.time() - t0) * 1000,
)
def is_available(self) -> bool:
# Available when at least one accessibility library can be found
if platform.system() == "Linux":
try:
import pyatspi # noqa: F401
return True
except ImportError:
pass
elif platform.system() == "Windows":
try:
import pywinauto # noqa: F401
return True
except ImportError:
pass
elif platform.system() == "Darwin":
try:
import ApplicationServices # noqa: F401
return True
except ImportError:
pass
return False
# ── Actions ──────────────────────────────────────────────────────────
def _shortcut(self, p: dict) -> dict:
"""Send a keyboard shortcut to the focused window."""
keys: str = p.get("keys", "")
if not keys:
raise ValueError("shortcut action requires 'keys' param.")
sys = platform.system()
if sys == "Linux":
return self._shortcut_xdotool(keys)
elif sys == "Windows":
return self._shortcut_win32(keys)
elif sys == "Darwin":
return self._shortcut_macos(keys)
raise NotImplementedError(
f"SemanticUIBackend.shortcut: not yet implemented for platform {sys}. "
"Consider using native_api backend instead."
)
def _shortcut_xdotool(self, keys: str) -> dict:
"""Use xdotool to send keys on Linux."""
import shutil
if not shutil.which("xdotool"):
raise NotImplementedError(
"xdotool not found. Install with: apt install xdotool"
)
import subprocess
# Convert from ctrl+shift+e → ctrl+shift+e (xdotool format)
xdg_keys = keys.replace("+", " ")
subprocess.run(["xdotool", "key", xdg_keys], check=True)
return {"keys": keys, "method": "xdotool"}
def _shortcut_win32(self, keys: str) -> dict:
raise NotImplementedError(
"SemanticUIBackend.shortcut: Windows requires pywinauto. "
"pip install pywinauto"
)
def _shortcut_macos(self, keys: str) -> dict:
raise NotImplementedError(
"SemanticUIBackend.shortcut: macOS requires pyobjc. "
"pip install pyobjc-framework-Quartz"
)
def _menu_click(self, p: dict) -> dict:
"""Click a menu item by path."""
menu_path: list = p.get("menu_path", [])
raise NotImplementedError(
f"SemanticUIBackend.menu_click: not yet implemented. "
f"Menu path: {menu_path}. "
"Use native_api or file_transform backends instead when possible."
)
def _wait_for_window(self, p: dict) -> dict:
"""Wait for a window matching title criteria."""
title_contains: str = p.get("title_contains", "")
timeout_ms: int = int(p.get("timeout_ms", 5000))
raise NotImplementedError(
f"SemanticUIBackend.wait_for_window: not yet implemented. "
f"Looking for: '{title_contains}'. "
"Consider using postconditions (file_exists) to detect completion."
)
def _button_click(self, p: dict) -> dict:
"""Click a UI button by label."""
raise NotImplementedError(
f"SemanticUIBackend.button_click: not yet implemented. "
"Use native_api or file_transform backends instead when possible."
)
def _type_text(self, p: dict) -> dict:
"""Type text into the focused input field."""
text: str = p.get("text", "")
if not text:
raise ValueError("type_text action requires 'text' param.")
sys = platform.system()
if sys == "Linux":
import shutil
if not shutil.which("xdotool"):
raise NotImplementedError("xdotool not found. apt install xdotool")
import subprocess
subprocess.run(["xdotool", "type", "--clearmodifiers", text], check=True)
return {"text": text, "method": "xdotool"}
raise NotImplementedError(
f"SemanticUIBackend.type_text: not yet implemented for {sys}."
)
@@ -0,0 +1,342 @@
"""Macro data model — parse and validate YAML macro definitions.
A macro definition file (YAML) describes a reusable, parameterized workflow
that the MacroRuntime can execute against any backend.
Example (minimal):
name: export_file
version: "1.0"
description: Export a file using the target app's CLI.
parameters:
output:
type: string
required: true
example: /tmp/out.png
steps:
- backend: native_api
action: run_command
params:
command: [echo, "exported", "${output}"]
postconditions:
- file_exists: ${output}
"""
from __future__ import annotations
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Optional
try:
import yaml
except ImportError as e:
raise ImportError("PyYAML is required: pip install PyYAML") from e
# ── Dataclasses ──────────────────────────────────────────────────────────────
@dataclass
class MacroParameter:
name: str
type: str = "string" # string | integer | boolean | list | dict
required: bool = False
default: Any = None
description: str = ""
example: Any = None
enum: Optional[list] = None
min: Optional[float] = None
max: Optional[float] = None
def validate_value(self, value: Any) -> list[str]:
"""Return list of validation error strings (empty if valid)."""
errors: list[str] = []
if value is None:
if self.required:
errors.append(f"Parameter '{self.name}' is required.")
return errors
if self.type == "integer":
if not isinstance(value, int):
try:
value = int(value)
except (ValueError, TypeError):
errors.append(f"Parameter '{self.name}' must be an integer.")
return errors
if self.min is not None and value < self.min:
errors.append(f"Parameter '{self.name}' must be >= {self.min}.")
if self.max is not None and value > self.max:
errors.append(f"Parameter '{self.name}' must be <= {self.max}.")
if self.enum and value not in self.enum:
errors.append(
f"Parameter '{self.name}' must be one of {self.enum}, got {value!r}."
)
return errors
@dataclass
class MacroStep:
backend: str # native_api | file_transform | semantic_ui | gui_macro | recovery
action: str # backend-specific action name
id: str = ""
params: dict = field(default_factory=dict)
timeout_ms: int = 30_000
on_failure: str = "fail" # fail | skip | continue
retry_max: int = 0
retry_backoff_ms: list[int] = field(default_factory=lambda: [1000])
def to_dict(self) -> dict:
return {
"id": self.id,
"backend": self.backend,
"action": self.action,
"params": self.params,
"timeout_ms": self.timeout_ms,
"on_failure": self.on_failure,
"retry_max": self.retry_max,
"retry_backoff_ms": self.retry_backoff_ms,
}
@dataclass
class MacroCondition:
"""A single pre- or post-condition check.
Supported types (derived from the YAML key):
file_exists: <path>
file_size_gt: [<path>, <bytes>]
process_running: <name>
env_var: <name>
always: true | false
"""
type: str
args: Any # depends on type
def to_dict(self) -> dict:
return {"type": self.type, "args": self.args}
@classmethod
def from_dict(cls, d: dict) -> "MacroCondition":
"""Parse a condition dict like {file_exists: /tmp/out.png}."""
if not isinstance(d, dict) or len(d) != 1:
raise ValueError(f"Condition must be a single-key dict, got: {d!r}")
ctype, args = next(iter(d.items()))
return cls(type=ctype, args=args)
@dataclass
class MacroOutput:
name: str
description: str = ""
path: Optional[str] = None # raw template string (may contain ${...})
value: Optional[Any] = None
def to_dict(self) -> dict:
return {
"name": self.name,
"description": self.description,
"path": self.path,
"value": self.value,
}
@dataclass
class MacroDefinition:
name: str
version: str = "1.0"
description: str = ""
parameters: dict[str, MacroParameter] = field(default_factory=dict)
preconditions: list[MacroCondition] = field(default_factory=list)
steps: list[MacroStep] = field(default_factory=list)
postconditions: list[MacroCondition] = field(default_factory=list)
outputs: list[MacroOutput] = field(default_factory=list)
tags: list[str] = field(default_factory=list)
composable: bool = False
agent_hints: dict = field(default_factory=dict)
source_path: str = "" # absolute path to the .yaml file
# ── Validation ────────────────────────────────────────────────────
def validate(self) -> list[str]:
"""Structural validation — returns list of error strings."""
errors: list[str] = []
if not self.name:
errors.append("Macro name is required.")
if not self.steps:
errors.append(f"Macro '{self.name}' has no steps.")
valid_backends = {"native_api", "file_transform", "semantic_ui", "gui_macro", "recovery"}
for i, step in enumerate(self.steps):
if step.backend not in valid_backends:
errors.append(
f"Step {i} has unknown backend '{step.backend}'. "
f"Valid: {sorted(valid_backends)}"
)
if not step.action:
errors.append(f"Step {i} (backend={step.backend}) is missing 'action'.")
for pname, pspec in self.parameters.items():
if pspec.type not in ("string", "integer", "boolean", "list", "dict", "float"):
errors.append(f"Parameter '{pname}' has unknown type '{pspec.type}'.")
return errors
def validate_params(self, params: dict) -> list[str]:
"""Validate runtime parameter values against schema."""
errors: list[str] = []
for pname, pspec in self.parameters.items():
value = params.get(pname, pspec.default)
errors.extend(pspec.validate_value(value))
return errors
def resolve_params(self, params: dict) -> dict:
"""Return params with defaults filled in."""
resolved = {}
for pname, pspec in self.parameters.items():
resolved[pname] = params.get(pname, pspec.default)
# Pass through any extra params not in schema
for k, v in params.items():
if k not in resolved:
resolved[k] = v
return resolved
# ── Serialisation ─────────────────────────────────────────────────
def to_dict(self) -> dict:
return {
"name": self.name,
"version": self.version,
"description": self.description,
"parameters": {
n: {
"type": p.type,
"required": p.required,
"default": p.default,
"description": p.description,
"example": p.example,
"enum": p.enum,
}
for n, p in self.parameters.items()
},
"preconditions": [c.to_dict() for c in self.preconditions],
"steps": [s.to_dict() for s in self.steps],
"postconditions": [c.to_dict() for c in self.postconditions],
"outputs": [o.to_dict() for o in self.outputs],
"tags": self.tags,
"composable": self.composable,
"agent_hints": self.agent_hints,
"source_path": self.source_path,
}
# ── Substitution ─────────────────────────────────────────────────────────────
_SUBST_RE = re.compile(r"\$\{([^}]+)\}")
def substitute(template: Any, params: dict) -> Any:
"""Replace ${key} placeholders in strings (and nested structures).
Works recursively on strings, lists, and dicts.
Leaves non-string types (int, bool, None) untouched.
"""
if isinstance(template, str):
def _replace(m: re.Match) -> str:
key = m.group(1).strip()
val = params.get(key)
return str(val) if val is not None else m.group(0)
return _SUBST_RE.sub(_replace, template)
if isinstance(template, list):
return [substitute(item, params) for item in template]
if isinstance(template, dict):
return {k: substitute(v, params) for k, v in template.items()}
return template
# ── YAML loader ───────────────────────────────────────────────────────────────
def _parse_parameter(name: str, raw: dict) -> MacroParameter:
return MacroParameter(
name=name,
type=raw.get("type", "string"),
required=raw.get("required", False),
default=raw.get("default"),
description=raw.get("description", ""),
example=raw.get("example"),
enum=raw.get("enum"),
min=raw.get("min"),
max=raw.get("max"),
)
def _parse_step(i: int, raw: dict) -> MacroStep:
retry = raw.get("retry_policy", {}) or {}
return MacroStep(
id=raw.get("id", f"step_{i}"),
backend=raw.get("backend", "native_api"),
action=raw.get("action", ""),
params=raw.get("params", {}),
timeout_ms=int(raw.get("timeout_ms", raw.get("timeout", "30s")
.replace("s", "000") if isinstance(raw.get("timeout"), str)
else raw.get("timeout_ms", 30_000))),
on_failure=raw.get("on_failure", "fail"),
retry_max=retry.get("max_retries", raw.get("retry_max", 0)),
retry_backoff_ms=retry.get("backoff_ms", [1000]),
)
def _parse_condition(raw: Any) -> MacroCondition:
if isinstance(raw, dict):
return MacroCondition.from_dict(raw)
raise ValueError(f"Cannot parse condition from {raw!r}")
def _parse_output(raw: dict) -> MacroOutput:
return MacroOutput(
name=raw.get("name", ""),
description=raw.get("description", ""),
path=raw.get("path"),
value=raw.get("value"),
)
def load_from_yaml(path: str) -> MacroDefinition:
"""Load and parse a macro definition from a YAML file."""
p = Path(path)
if not p.is_file():
raise FileNotFoundError(f"Macro file not found: {path}")
with open(p, encoding="utf-8") as f:
raw = yaml.safe_load(f)
if not isinstance(raw, dict):
raise ValueError(f"Macro YAML must be a mapping, got {type(raw).__name__}: {path}")
parameters: dict[str, MacroParameter] = {}
for pname, praw in (raw.get("parameters") or {}).items():
if isinstance(praw, dict):
parameters[pname] = _parse_parameter(pname, praw)
else:
# shorthand: parameter_name: string
parameters[pname] = MacroParameter(name=pname, type=str(praw))
steps = [_parse_step(i, s) for i, s in enumerate(raw.get("steps") or [])]
preconditions = [_parse_condition(c) for c in (raw.get("preconditions") or [])]
postconditions = [_parse_condition(c) for c in (raw.get("postconditions") or [])]
outputs = [_parse_output(o) for o in (raw.get("outputs") or [])]
macro = MacroDefinition(
name=raw.get("name", p.stem),
version=str(raw.get("version", "1.0")),
description=raw.get("description", ""),
parameters=parameters,
preconditions=preconditions,
steps=steps,
postconditions=postconditions,
outputs=outputs,
tags=raw.get("tags", []),
composable=raw.get("composable", False),
agent_hints=raw.get("agent_hints", {}),
source_path=str(p.resolve()),
)
return macro
@@ -0,0 +1,164 @@
"""MacroRegistry — discovers and loads macro definitions from a directory.
The registry scans a macros/ directory (and subdirectories) for *.yaml files,
optionally guided by a manifest.yaml index.
Usage:
from cli_anything.openclaw.core.registry import MacroRegistry
registry = MacroRegistry("/path/to/macros")
macro = registry.load("export_file")
all_macros = registry.list_all()
"""
from __future__ import annotations
import os
from pathlib import Path
from typing import Optional
try:
import yaml
except ImportError as e:
raise ImportError("PyYAML is required: pip install PyYAML") from e
from cli_anything.openclaw.core.macro_model import MacroDefinition, load_from_yaml
class MacroRegistry:
"""Discovers and caches macro definitions from a macros/ directory."""
def __init__(self, macros_dir: Optional[str] = None):
"""
Args:
macros_dir: Path to the directory containing macro YAML files.
Defaults to the macros/ directory bundled with the package.
"""
if macros_dir is None:
macros_dir = str(Path(__file__).resolve().parent.parent / "macro_definitions")
self.macros_dir = Path(macros_dir)
self._cache: dict[str, MacroDefinition] = {}
self._scanned = False
# ── Internal scan ────────────────────────────────────────────────────
def _scan(self) -> None:
"""Scan macros_dir and populate the cache."""
if self._scanned:
return
if not self.macros_dir.is_dir():
self._scanned = True
return
# Try manifest.yaml first (explicit ordered index)
manifest_path = self.macros_dir / "manifest.yaml"
if manifest_path.is_file():
self._load_from_manifest(manifest_path)
else:
# Fallback: scan all *.yaml files recursively (except manifest.yaml)
for yaml_path in sorted(self.macros_dir.rglob("*.yaml")):
if yaml_path.name == "manifest.yaml":
continue
self._load_file(yaml_path)
self._scanned = True
def _load_from_manifest(self, manifest_path: Path) -> None:
"""Load macros listed in manifest.yaml."""
with open(manifest_path, encoding="utf-8") as f:
manifest = yaml.safe_load(f) or {}
macros_list = manifest.get("macros", [])
for entry in macros_list:
if isinstance(entry, dict):
rel_path = entry.get("path")
else:
rel_path = str(entry)
if not rel_path:
continue
yaml_path = self.macros_dir / rel_path
if yaml_path.is_file():
self._load_file(yaml_path)
# Also scan for any yaml files NOT in the manifest (permissive)
listed_names = {m.name for m in self._cache.values()}
for yaml_path in sorted(self.macros_dir.rglob("*.yaml")):
if yaml_path.name == "manifest.yaml":
continue
try:
# Quick peek to get the name without full parse
with open(yaml_path, encoding="utf-8") as f:
raw = yaml.safe_load(f) or {}
name = raw.get("name", yaml_path.stem)
if name not in listed_names:
self._load_file(yaml_path)
except Exception:
pass
def _load_file(self, yaml_path: Path) -> Optional[MacroDefinition]:
"""Parse one yaml file and cache the result."""
try:
macro = load_from_yaml(str(yaml_path))
self._cache[macro.name] = macro
return macro
except Exception as exc:
# Log but don't crash — bad macros should not block the registry
import sys
print(f"[registry] Warning: failed to load {yaml_path}: {exc}", file=sys.stderr)
return None
# ── Public API ───────────────────────────────────────────────────────
def load(self, name: str) -> MacroDefinition:
"""Load a macro by name.
Raises:
KeyError: if the macro is not found.
"""
self._scan()
if name not in self._cache:
available = sorted(self._cache.keys())
raise KeyError(
f"Macro '{name}' not found. Available: {available}"
)
return self._cache[name]
def list_all(self) -> list[MacroDefinition]:
"""Return all loaded macro definitions, sorted by name."""
self._scan()
return sorted(self._cache.values(), key=lambda m: m.name)
def list_names(self) -> list[str]:
"""Return all macro names, sorted."""
self._scan()
return sorted(self._cache.keys())
def reload(self, name: Optional[str] = None) -> None:
"""Force reload from disk.
Args:
name: If given, reload just that macro file.
If None, rescan the entire directory.
"""
if name is None:
self._cache.clear()
self._scanned = False
self._scan()
elif name in self._cache:
path = self._cache[name].source_path
if path and Path(path).is_file():
self._load_file(Path(path))
def register(self, macro: MacroDefinition) -> None:
"""Programmatically register a macro (e.g. from tests)."""
self._cache[macro.name] = macro
self._scanned = True # Don't re-scan over in-memory registrations
def info(self) -> dict:
"""Return registry metadata."""
self._scan()
return {
"macros_dir": str(self.macros_dir),
"total": len(self._cache),
"names": self.list_names(),
}
@@ -0,0 +1,113 @@
"""RoutingEngine — select the best backend for each macro step.
Priority order (higher = preferred):
native_api 100
gui_macro 80
file_transform 70
semantic_ui 50
recovery 10 (only via explicit backend: recovery, or auto-retry)
The router respects the step's explicit `backend:` field.
It then checks availability; if the primary is unavailable it walks down
the priority list.
"""
from __future__ import annotations
from cli_anything.openclaw.backends.base import Backend, BackendContext, StepResult
from cli_anything.openclaw.backends.native_api import NativeAPIBackend
from cli_anything.openclaw.backends.file_transform import FileTransformBackend
from cli_anything.openclaw.backends.semantic_ui import SemanticUIBackend
from cli_anything.openclaw.backends.gui_macro import GUIMacroBackend
from cli_anything.openclaw.backends.recovery import RecoveryBackend
from cli_anything.openclaw.core.macro_model import MacroStep
_BACKEND_PRIORITY: dict[str, int] = {
"native_api": 100,
"gui_macro": 80,
"file_transform": 70,
"semantic_ui": 50,
"recovery": 10,
}
class RoutingEngine:
"""Selects and manages execution backends for macro steps."""
def __init__(self):
self._recovery = RecoveryBackend()
self._backends: dict[str, Backend] = {
"native_api": NativeAPIBackend(),
"file_transform": FileTransformBackend(),
"semantic_ui": SemanticUIBackend(),
"gui_macro": GUIMacroBackend(),
"recovery": self._recovery,
}
# Wire recovery with all other backends so it can delegate
for b in self._backends.values():
self._recovery.register_backend(b)
def select(self, step: MacroStep) -> Backend:
"""Return the best available backend for the given step.
Respects step.backend if set; falls back down the priority list
if that backend is unavailable.
Raises:
RuntimeError: if no backend is available.
"""
requested = step.backend
# Try the requested backend first
if requested in self._backends:
b = self._backends[requested]
if b.is_available():
return b
# Walk by priority (descending) for a fallback
for name in sorted(_BACKEND_PRIORITY, key=lambda k: -_BACKEND_PRIORITY[k]):
if name == "recovery":
continue # Never auto-fall-through to recovery
b = self._backends.get(name)
if b and b.is_available():
return b
raise RuntimeError(
f"No backend available for step '{step.id}' "
f"(requested: '{requested}'). "
"Check that required tools are installed."
)
def execute_step(
self,
step: MacroStep,
params: dict,
context: BackendContext,
) -> StepResult:
"""Route and execute a step, applying retry logic if configured."""
backend = self.select(step)
if step.retry_max <= 0:
return backend.execute(step, params, context)
# Retry with backoff
last_result: StepResult | None = None
backoff = step.retry_backoff_ms or [1000]
for attempt in range(step.retry_max + 1):
last_result = backend.execute(step, params, context)
if last_result.success:
return last_result
if attempt < step.retry_max:
import time
wait = backoff[min(attempt, len(backoff) - 1)] / 1000.0
time.sleep(wait)
assert last_result is not None
return last_result
def describe(self) -> dict:
"""Return a description of all registered backends and their status."""
return {
name: b.describe()
for name, b in self._backends.items()
}
@@ -0,0 +1,306 @@
"""MacroRuntime — orchestrates the full macro execution lifecycle.
Lifecycle for execute(macro_name, params):
1. Load macro definition from registry
2. Resolve + validate parameters (fill defaults, type-check)
3. Check preconditions
4. For each step:
a. substitute ${params} into step.params
b. route to backend
c. execute (with retry if configured)
d. handle on_failure = fail | skip | continue
5. Check postconditions
6. Collect declared outputs
7. Record telemetry in session
8. Return ExecutionResult
"""
from __future__ import annotations
import os
import time
from dataclasses import dataclass, field
from typing import Any, Optional
from cli_anything.openclaw.core.macro_model import (
MacroCondition,
MacroDefinition,
MacroStep,
substitute,
)
from cli_anything.openclaw.core.registry import MacroRegistry
from cli_anything.openclaw.core.routing import RoutingEngine
from cli_anything.openclaw.core.session import ExecutionSession, RunRecord
from cli_anything.openclaw.backends.base import BackendContext, StepResult
# ── Result types ─────────────────────────────────────────────────────────────
@dataclass
class ExecutionResult:
success: bool
macro_name: str
output: dict = field(default_factory=dict)
error: str = ""
step_results: list[StepResult] = field(default_factory=list)
telemetry: dict = field(default_factory=dict)
def to_dict(self) -> dict:
return {
"success": self.success,
"macro_name": self.macro_name,
"output": self.output,
"error": self.error,
"telemetry": self.telemetry,
"steps": [s.to_dict() for s in self.step_results],
}
# ── Condition checker ────────────────────────────────────────────────────────
def _check_condition(cond: MacroCondition, resolved_params: dict) -> Optional[str]:
"""Evaluate one condition.
Returns None if the condition passes, or an error string if it fails.
"""
ctype = cond.type
args = substitute(cond.args, resolved_params)
if ctype == "file_exists":
path = str(args)
if not os.path.exists(path):
return f"file_exists: '{path}' not found."
return None
elif ctype == "file_size_gt":
if not isinstance(args, (list, tuple)) or len(args) < 2:
return f"file_size_gt: expected [path, min_bytes], got {args!r}"
path, min_bytes = str(args[0]), int(args[1])
if not os.path.exists(path):
return f"file_size_gt: '{path}' not found."
size = os.path.getsize(path)
if size <= min_bytes:
return f"file_size_gt: '{path}' is {size} bytes, expected > {min_bytes}."
return None
elif ctype == "process_running":
name = str(args)
# Try pgrep first, then psutil
import shutil
import subprocess
if shutil.which("pgrep"):
r = subprocess.run(["pgrep", "-x", name], capture_output=True)
if r.returncode == 0:
return None
return f"process_running: '{name}' not found (pgrep)."
try:
import psutil
for proc in psutil.process_iter(["name"]):
if proc.info["name"] == name:
return None
return f"process_running: '{name}' not found."
except ImportError:
# Can't verify — let it pass with a warning
return None
elif ctype == "env_var":
name = str(args)
if name not in os.environ:
return f"env_var: '{name}' is not set in the environment."
return None
elif ctype == "always":
if str(args).lower() in ("false", "0", "no"):
return "always: false condition."
return None
else:
# Unknown condition type — warn but don't block
return None
# ── Runtime ──────────────────────────────────────────────────────────────────
class MacroRuntime:
"""Executes macros end-to-end."""
def __init__(
self,
registry: Optional[MacroRegistry] = None,
routing_engine: Optional[RoutingEngine] = None,
session: Optional[ExecutionSession] = None,
):
self.registry = registry or MacroRegistry()
self.routing = routing_engine or RoutingEngine()
self.session = session or ExecutionSession()
# ── Public API ───────────────────────────────────────────────────────
def execute(
self,
macro_name: str,
params: dict,
dry_run: bool = False,
) -> ExecutionResult:
"""Execute a macro by name with the given parameters.
Args:
macro_name: Name of the macro to execute.
params: Input parameters (raw, may be strings from CLI).
dry_run: If True, skip all side effects and return simulated success.
Returns:
ExecutionResult with success status, outputs, and telemetry.
"""
t0 = time.time()
# 1. Load macro
try:
macro = self.registry.load(macro_name)
except KeyError as exc:
return ExecutionResult(
success=False, macro_name=macro_name, error=str(exc)
)
# 2. Resolve + validate params
resolved = macro.resolve_params(params)
param_errors = macro.validate_params(resolved)
if param_errors:
return ExecutionResult(
success=False,
macro_name=macro_name,
error="Parameter validation failed:\n" + "\n".join(f" - {e}" for e in param_errors),
)
# 3. Check preconditions
precond_errors = self.check_conditions(macro.preconditions, resolved)
if precond_errors:
return ExecutionResult(
success=False,
macro_name=macro_name,
error="Preconditions not met:\n" + "\n".join(f" - {e}" for e in precond_errors),
)
# 4. Execute steps
step_results: list[StepResult] = []
context = BackendContext(
params=resolved,
previous_results=step_results,
dry_run=dry_run,
)
aborted = False
abort_error = ""
for step in macro.steps:
context.timeout_ms = step.timeout_ms
try:
result = self.routing.execute_step(step, resolved, context)
except Exception as exc:
result = StepResult(
success=False,
error=f"Unhandled exception in step '{step.id}': {exc}",
backend_used=step.backend,
)
step_results.append(result)
if not result.success:
if step.on_failure == "fail":
aborted = True
abort_error = f"Step '{step.id}' failed: {result.error}"
break
elif step.on_failure == "skip":
continue
# on_failure == "continue" — keep going regardless
# 5. Check postconditions (skip if already failed)
postcond_errors: list[str] = []
if not aborted:
postcond_errors = self.check_conditions(macro.postconditions, resolved)
success = not aborted and not postcond_errors
# 6. Collect outputs
output = self._collect_outputs(macro, resolved, step_results) if success else {}
# 7. Build error string
error = ""
if aborted:
error = abort_error
elif postcond_errors:
error = "Postconditions failed:\n" + "\n".join(f" - {e}" for e in postcond_errors)
# 8. Telemetry
duration_ms = (time.time() - t0) * 1000
backends_used = list({r.backend_used for r in step_results if r.backend_used})
telemetry = {
"duration_ms": duration_ms,
"steps_total": len(macro.steps),
"steps_run": len(step_results),
"backends_used": backends_used,
"dry_run": dry_run,
}
# 9. Record in session
record = RunRecord(
macro_name=macro_name,
params=params,
success=success,
output=output,
error=error,
duration_ms=duration_ms,
backends_used=backends_used,
steps_run=len(step_results),
)
self.session.record(record)
return ExecutionResult(
success=success,
macro_name=macro_name,
output=output,
error=error,
step_results=step_results,
telemetry=telemetry,
)
def check_conditions(
self,
conditions: list[MacroCondition],
params: dict,
) -> list[str]:
"""Evaluate a list of conditions; return list of failure messages."""
errors: list[str] = []
for cond in conditions:
err = _check_condition(cond, params)
if err:
errors.append(err)
return errors
def validate_macro(self, macro_name: str) -> list[str]:
"""Load and structurally validate a macro; return error list."""
try:
macro = self.registry.load(macro_name)
except KeyError as exc:
return [str(exc)]
return macro.validate()
# ── Helpers ──────────────────────────────────────────────────────────
def _collect_outputs(
self,
macro: MacroDefinition,
params: dict,
step_results: list[StepResult],
) -> dict:
"""Resolve declared macro outputs into a concrete dict."""
out: dict[str, Any] = {}
for output_spec in macro.outputs:
name = output_spec.name
if output_spec.path:
out[name] = substitute(output_spec.path, params)
elif output_spec.value is not None:
out[name] = substitute(output_spec.value, params)
# Always include combined step outputs under 'steps'
out["_steps"] = [r.output for r in step_results]
return out
@@ -0,0 +1,186 @@
"""ExecutionSession — tracks macro run history and telemetry.
Persists run records to ~/.openclaw-macro/sessions/ so agents can inspect
what was run, what succeeded, and what the outputs were.
"""
from __future__ import annotations
import json
import os
import time
from pathlib import Path
from typing import Optional
SESSION_DIR = Path.home() / ".openclaw-macro" / "sessions"
MAX_HISTORY = 200
def _locked_save_json(path: str, data, **dump_kwargs) -> None:
"""Atomically write JSON with exclusive file locking."""
try:
f = open(path, "r+")
except FileNotFoundError:
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
f = open(path, "w")
with f:
_locked = False
try:
import fcntl
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
_locked = True
except (ImportError, OSError):
pass
try:
f.seek(0)
f.truncate()
json.dump(data, f, **dump_kwargs)
f.flush()
finally:
if _locked:
import fcntl
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
class RunRecord:
"""A single macro execution record."""
def __init__(
self,
macro_name: str,
params: dict,
success: bool,
output: dict,
error: str,
duration_ms: float,
backends_used: list[str],
steps_run: int,
timestamp: Optional[float] = None,
):
self.macro_name = macro_name
self.params = params
self.success = success
self.output = output
self.error = error
self.duration_ms = duration_ms
self.backends_used = backends_used
self.steps_run = steps_run
self.timestamp = timestamp or time.time()
def to_dict(self) -> dict:
return {
"macro_name": self.macro_name,
"params": self.params,
"success": self.success,
"output": self.output,
"error": self.error,
"duration_ms": self.duration_ms,
"backends_used": self.backends_used,
"steps_run": self.steps_run,
"timestamp": self.timestamp,
}
@classmethod
def from_dict(cls, d: dict) -> "RunRecord":
return cls(
macro_name=d.get("macro_name", ""),
params=d.get("params", {}),
success=d.get("success", False),
output=d.get("output", {}),
error=d.get("error", ""),
duration_ms=d.get("duration_ms", 0),
backends_used=d.get("backends_used", []),
steps_run=d.get("steps_run", 0),
timestamp=d.get("timestamp"),
)
class ExecutionSession:
"""Tracks macro run history for the current session."""
def __init__(self, session_id: Optional[str] = None):
self.session_id = session_id or f"session_{int(time.time())}"
self._history: list[RunRecord] = []
# ── Record management ─────────────────────────────────────────────
def record(self, run: RunRecord) -> None:
"""Add a run record to history."""
self._history.append(run)
if len(self._history) > MAX_HISTORY:
self._history = self._history[-MAX_HISTORY:]
def last(self) -> Optional[RunRecord]:
"""Return the most recent run record."""
return self._history[-1] if self._history else None
def history(self, limit: int = 20) -> list[RunRecord]:
"""Return recent run records, newest first."""
return list(reversed(self._history[-limit:]))
def stats(self) -> dict:
"""Return aggregate statistics for this session."""
total = len(self._history)
if total == 0:
return {"total": 0, "success_rate": 0.0, "avg_duration_ms": 0.0}
successes = sum(1 for r in self._history if r.success)
avg_dur = sum(r.duration_ms for r in self._history) / total
return {
"total": total,
"success": successes,
"failure": total - successes,
"success_rate": successes / total,
"avg_duration_ms": avg_dur,
}
def status(self) -> dict:
return {
"session_id": self.session_id,
"runs": len(self._history),
**self.stats(),
}
# ── Persistence ───────────────────────────────────────────────────
def save(self) -> str:
"""Persist session to disk. Returns the file path."""
SESSION_DIR.mkdir(parents=True, exist_ok=True)
path = str(SESSION_DIR / f"{self.session_id}.json")
data = {
"session_id": self.session_id,
"timestamp": time.time(),
"history": [r.to_dict() for r in self._history],
}
_locked_save_json(path, data, indent=2, sort_keys=True)
return path
@classmethod
def load(cls, session_id: str) -> Optional["ExecutionSession"]:
"""Load a session from disk."""
path = SESSION_DIR / f"{session_id}.json"
if not path.is_file():
return None
with open(path, encoding="utf-8") as f:
data = json.load(f)
session = cls(session_id=data.get("session_id", session_id))
session._history = [RunRecord.from_dict(r) for r in data.get("history", [])]
return session
@classmethod
def list_sessions(cls) -> list[dict]:
"""List all saved sessions (metadata only)."""
SESSION_DIR.mkdir(parents=True, exist_ok=True)
sessions = []
for p in SESSION_DIR.glob("*.json"):
try:
with open(p, encoding="utf-8") as f:
d = json.load(f)
sessions.append({
"session_id": d.get("session_id"),
"timestamp": d.get("timestamp", 0),
"runs": len(d.get("history", [])),
})
except Exception:
continue
sessions.sort(key=lambda s: s.get("timestamp", 0), reverse=True)
return sessions
@@ -0,0 +1,63 @@
name: export_file
version: "1.0"
description: >
Export a file from the target application using its native CLI.
Demonstrates the native_api backend with a run_command action.
Replace the command with your actual application's export command.
tags: [export, native_api, example]
parameters:
output:
type: string
required: true
description: Destination file path for the export.
example: /tmp/exported.txt
input:
type: string
required: false
default: ""
description: Source file to export from (optional, app-dependent).
format:
type: string
required: false
default: plain
description: Output format.
enum: [plain, json, csv]
preconditions:
- file_exists: /bin/echo # sanity check: a real macro would check the app binary
steps:
- id: step_export
backend: native_api
action: run_command
params:
# Replace this with your real export command, e.g.:
# command: [inkscape, --export-filename, "${output}", "${input}"]
command: [echo, "Exported to ${output} (format=${format})"]
capture_stdout: true
timeout_ms: 30000
on_failure: fail
- id: step_write_output
backend: file_transform
action: text_replace
params:
input_file: /dev/null
output_file: ${output}
find: ""
replace: ""
on_failure: skip # Writing the marker file is best-effort
postconditions: []
outputs:
- name: exported_file
path: ${output}
description: Path where the export was written.
agent_hints:
danger_level: safe
side_effects: [creates_file]
reversible: true
estimated_duration_ms: 2000
@@ -0,0 +1,52 @@
name: transform_json
version: "1.0"
description: >
Read a JSON file, set a nested key to a new value, and write it back.
Demonstrates the file_transform backend with json_set action.
tags: [json, file_transform, example]
parameters:
file:
type: string
required: true
description: Path to the JSON file to transform.
example: /tmp/config.json
key:
type: string
required: true
description: Dot-separated key path to set (e.g. settings.theme).
example: settings.theme
value:
type: string
required: true
description: Value to write at the key path.
example: dark
preconditions:
- file_exists: ${file}
steps:
- id: step_set_key
backend: file_transform
action: json_set
params:
input_file: ${file}
output_file: ${file}
path: ${key}
value: ${value}
timeout_ms: 10000
on_failure: fail
postconditions:
- file_exists: ${file}
outputs:
- name: modified_file
path: ${file}
description: Path to the modified JSON file.
agent_hints:
danger_level: moderate
side_effects: [modifies_file]
reversible: false
estimated_duration_ms: 100
@@ -0,0 +1,37 @@
name: undo_last
version: "1.0"
description: >
Trigger an undo operation in the target application using a keyboard shortcut
(Ctrl+Z). This macro demonstrates the semantic_ui backend.
On Linux, requires xdotool. On macOS/Windows, adapt to your platform.
tags: [undo, semantic_ui, example]
parameters:
app_name:
type: string
required: false
default: ""
description: Name of the target application process (used to verify it is running).
example: inkscape
preconditions:
- always: true # process check is optional in this example
steps:
- id: step_undo
backend: semantic_ui
action: shortcut
params:
keys: ctrl+z
timeout_ms: 5000
on_failure: fail
postconditions: []
outputs: []
agent_hints:
danger_level: safe
side_effects: [modifies_app_state]
reversible: true # can be redone with ctrl+y
estimated_duration_ms: 500
@@ -0,0 +1,10 @@
macros:
- name: export_file
path: examples/export_file.yaml
version: "1.0"
- name: undo_last
path: examples/undo_last.yaml
version: "1.0"
- name: transform_json
path: examples/transform_json.yaml
version: "1.0"
@@ -0,0 +1,572 @@
#!/usr/bin/env python3
"""OpenClaw CLI — agent-callable interface for the Macro System.
This CLI is the L6 "Unified CLI Entry" in the OpenClaw Macro System.
It provides a stable, machine-readable interface for AI agents and
power users to invoke macros without touching the GUI.
Usage (one-shot):
cli-anything-openclaw macro run export_file --param output=/tmp/out.png --json
cli-anything-openclaw macro list --json
cli-anything-openclaw macro info export_file --json
Usage (REPL):
cli-anything-openclaw # enters interactive REPL
cli-anything-openclaw repl
"""
import sys
import os
import json
import click
from typing import Optional
from cli_anything.openclaw.core.registry import MacroRegistry
from cli_anything.openclaw.core.runtime import MacroRuntime
from cli_anything.openclaw.core.session import ExecutionSession
# ── Global state ─────────────────────────────────────────────────────────────
_json_output = False
_repl_mode = False
_dry_run = False
_session: Optional[ExecutionSession] = None
_runtime: Optional[MacroRuntime] = None
def get_runtime() -> MacroRuntime:
global _runtime, _session
if _runtime is None:
_session = _session or ExecutionSession()
_runtime = MacroRuntime(session=_session)
return _runtime
def get_session() -> ExecutionSession:
global _session
if _session is None:
_session = ExecutionSession()
return _session
# ── Output helpers ────────────────────────────────────────────────────────────
def output(data, message: str = ""):
"""Print result: JSON in --json mode, human-readable otherwise."""
if _json_output:
click.echo(json.dumps(data, indent=2, default=str))
else:
if message:
click.echo(message)
_print_value(data)
def _print_value(val, indent: int = 0):
prefix = " " * indent
if isinstance(val, dict):
for k, v in val.items():
if isinstance(v, (dict, list)):
click.echo(f"{prefix}{k}:")
_print_value(v, indent + 1)
else:
click.echo(f"{prefix}{k}: {v}")
elif isinstance(val, list):
for i, item in enumerate(val):
if isinstance(item, dict):
click.echo(f"{prefix}[{i}]")
_print_value(item, indent + 1)
else:
click.echo(f"{prefix}- {item}")
else:
click.echo(f"{prefix}{val}")
def handle_error(func):
"""Decorator: consistent error handling across commands."""
import functools
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except KeyError as e:
msg = str(e).strip("'\"")
if _json_output:
click.echo(json.dumps({"error": msg, "type": "not_found"}))
else:
click.echo(f"Error: {msg}", err=True)
if not _repl_mode:
sys.exit(1)
except FileNotFoundError as e:
if _json_output:
click.echo(json.dumps({"error": str(e), "type": "file_not_found"}))
else:
click.echo(f"Error: {e}", err=True)
if not _repl_mode:
sys.exit(1)
except Exception as e:
if _json_output:
click.echo(json.dumps({"error": str(e), "type": type(e).__name__}))
else:
click.echo(f"Error: {e}", err=True)
if not _repl_mode:
sys.exit(1)
return wrapper
# ── Parameter parsing ─────────────────────────────────────────────────────────
def _parse_params(param_tuples: tuple) -> dict:
"""Convert --param key=value tuples to a dict."""
result = {}
for pair in param_tuples:
if "=" in pair:
k, v = pair.split("=", 1)
result[k.strip()] = v.strip()
else:
click.echo(f"Warning: --param '{pair}' ignored (expected key=value format).", err=True)
return result
# ── Main CLI group ────────────────────────────────────────────────────────────
@click.group(invoke_without_command=True)
@click.option("--json", "json_flag", is_flag=True, help="Machine-readable JSON output.")
@click.option("--dry-run", "dry_run_flag", is_flag=True,
help="Simulate execution without side effects.")
@click.option("--session-id", default=None, help="Resume or create a named session.")
@click.pass_context
def cli(ctx, json_flag, dry_run_flag, session_id):
"""OpenClaw Macro System — run GUI workflows as CLI commands.
\b
Quick start:
cli-anything-openclaw macro list
cli-anything-openclaw macro info <name>
cli-anything-openclaw macro run <name> --param key=value
Enter interactive REPL by running without arguments.
"""
global _json_output, _dry_run, _session
_json_output = json_flag
_dry_run = dry_run_flag
if session_id:
loaded = ExecutionSession.load(session_id)
_session = loaded or ExecutionSession(session_id=session_id)
ctx.ensure_object(dict)
if ctx.invoked_subcommand is None:
ctx.invoke(repl)
# ── macro group ──────────────────────────────────────────────────────────────
@cli.group()
def macro():
"""Macro management and execution."""
@macro.command("run")
@click.argument("name")
@click.option("--param", "-p", multiple=True,
help="Macro parameter in key=value format. Repeat for multiple.")
@handle_error
def macro_run(name, param):
"""Execute a macro by name.
\b
Example:
macro run export_file --param output=/tmp/out.txt
macro run export_file -p output=/tmp/out.txt -p format=plain --json
"""
params = _parse_params(param)
runtime = get_runtime()
result = runtime.execute(name, params, dry_run=_dry_run)
if _json_output:
output(result.to_dict())
else:
if result.success:
click.echo(f"✓ Macro '{name}' completed successfully.")
if result.output:
for k, v in result.output.items():
if not k.startswith("_"):
click.echo(f" {k}: {v}")
else:
click.echo(f"✗ Macro '{name}' failed.", err=True)
click.echo(f" {result.error}", err=True)
if result.telemetry:
click.echo(
f" [{result.telemetry.get('duration_ms', 0):.0f}ms, "
f"backends: {', '.join(result.telemetry.get('backends_used', []))}]"
)
if not result.success and not _repl_mode:
sys.exit(1)
@macro.command("list")
@handle_error
def macro_list():
"""List all available macros."""
runtime = get_runtime()
macros = runtime.registry.list_all()
if _json_output:
output([{
"name": m.name,
"version": m.version,
"description": m.description,
"tags": m.tags,
"parameters": list(m.parameters.keys()),
} for m in macros])
else:
if not macros:
click.echo("No macros found.")
return
click.echo(f"Available macros ({len(macros)}):\n")
for m in macros:
tags = f" [{', '.join(m.tags)}]" if m.tags else ""
click.echo(f" {m.name:<30} {m.description}{tags}")
@macro.command("info")
@click.argument("name")
@handle_error
def macro_info(name):
"""Show full details for a macro (schema, parameters, steps)."""
runtime = get_runtime()
m = runtime.registry.load(name)
if _json_output:
output(m.to_dict())
else:
click.echo(f"\nMacro: {m.name} (v{m.version})")
click.echo(f" {m.description}\n")
if m.parameters:
click.echo("Parameters:")
for pname, pspec in m.parameters.items():
req = "(required)" if pspec.required else f"(default: {pspec.default!r})"
click.echo(f" --param {pname}=<{pspec.type}> {req}")
if pspec.description:
click.echo(f" {pspec.description}")
if m.preconditions:
click.echo(f"\nPreconditions ({len(m.preconditions)}):")
for c in m.preconditions:
click.echo(f" {c.type}: {c.args}")
if m.steps:
click.echo(f"\nSteps ({len(m.steps)}):")
for s in m.steps:
click.echo(f" [{s.id}] backend={s.backend} action={s.action}")
if m.postconditions:
click.echo(f"\nPostconditions ({len(m.postconditions)}):")
for c in m.postconditions:
click.echo(f" {c.type}: {c.args}")
if m.outputs:
click.echo(f"\nOutputs:")
for o in m.outputs:
click.echo(f" {o.name}: {o.path or o.value}")
if m.agent_hints:
click.echo(f"\nAgent hints: {m.agent_hints}")
click.echo()
@macro.command("validate")
@click.argument("name", required=False)
@handle_error
def macro_validate(name):
"""Validate macro definition(s). Pass a name or omit to validate all."""
runtime = get_runtime()
if name:
names = [name]
else:
names = runtime.registry.list_names()
results = {}
for n in names:
errors = runtime.validate_macro(n)
results[n] = errors
if _json_output:
output({n: {"valid": len(e) == 0, "errors": e} for n, e in results.items()})
else:
all_ok = True
for n, errors in results.items():
if errors:
all_ok = False
click.echo(f"{n}:")
for err in errors:
click.echo(f" - {err}", err=True)
else:
click.echo(f"{n}")
if all_ok:
click.echo("\nAll macros valid.")
else:
if not _repl_mode:
sys.exit(1)
@macro.command("dry-run")
@click.argument("name")
@click.option("--param", "-p", multiple=True, help="Parameter in key=value format.")
@handle_error
def macro_dry_run(name, param):
"""Simulate macro execution without any side effects."""
params = _parse_params(param)
runtime = get_runtime()
result = runtime.execute(name, params, dry_run=True)
if _json_output:
output(result.to_dict())
else:
click.echo(f"[dry-run] Macro '{name}'")
if result.success:
click.echo(" Would execute successfully.")
click.echo(f" Steps: {len(result.step_results)}")
else:
click.echo(f" Would fail: {result.error}", err=True)
@macro.command("define")
@click.argument("name")
@click.option("--output", "-o", default=None, help="Write YAML to this file path.")
@handle_error
def macro_define(name, output):
"""Scaffold a new macro YAML definition."""
import textwrap
template = textwrap.dedent(f"""\
name: {name}
version: "1.0"
description: "Describe what this macro does."
tags: []
parameters:
# Add your parameters here
# output:
# type: string
# required: true
# description: Output file path
# example: /tmp/result.txt
preconditions:
# Conditions that must be true before execution
# - file_exists: /path/to/input
# - process_running: my-app
steps:
- id: step_1
backend: native_api # or: file_transform, semantic_ui, gui_macro
action: run_command
params:
command: [echo, "Hello from {name}"]
timeout_ms: 30000
on_failure: fail # or: skip, continue
postconditions:
# Conditions verified after execution
# - file_exists: ${{output}}
outputs:
# Named outputs the agent can use
# - name: result_file
# path: ${{output}}
agent_hints:
danger_level: safe # safe | moderate | dangerous
side_effects: []
reversible: true
""")
if output:
from pathlib import Path
p = Path(output)
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(template, encoding="utf-8")
if _json_output:
click.echo(json.dumps({"created": str(p.resolve())}))
else:
click.echo(f"✓ Macro scaffold written to: {p.resolve()}")
else:
click.echo(template)
# ── session group ─────────────────────────────────────────────────────────────
@cli.group()
def session():
"""Session management and run history."""
@session.command("status")
@handle_error
def session_status():
"""Show current session status and statistics."""
sess = get_session()
data = sess.status()
output(data, "Session status:")
@session.command("history")
@click.option("--limit", default=10, show_default=True, help="Number of records to show.")
@handle_error
def session_history(limit):
"""Show recent macro execution history."""
sess = get_session()
records = sess.history(limit=limit)
if _json_output:
output([r.to_dict() for r in records])
else:
if not records:
click.echo("No runs recorded in this session.")
return
click.echo(f"Recent runs ({len(records)}):\n")
for r in records:
status = "" if r.success else ""
import datetime
ts = datetime.datetime.fromtimestamp(r.timestamp).strftime("%H:%M:%S")
click.echo(f" {status} [{ts}] {r.macro_name} ({r.duration_ms:.0f}ms)")
if not r.success:
click.echo(f" Error: {r.error}", err=True)
@session.command("save")
@handle_error
def session_save():
"""Persist current session to disk."""
sess = get_session()
path = sess.save()
output({"saved": path, "session_id": sess.session_id},
f"Session saved: {path}")
@session.command("list")
@handle_error
def session_list():
"""List all saved sessions."""
sessions = ExecutionSession.list_sessions()
if _json_output:
output(sessions)
else:
if not sessions:
click.echo("No saved sessions.")
return
click.echo("Saved sessions:\n")
for s in sessions:
import datetime
ts = datetime.datetime.fromtimestamp(s.get("timestamp", 0)).strftime("%Y-%m-%d %H:%M:%S")
click.echo(f" {s['session_id']} ({s['runs']} runs) {ts}")
# ── backends command ──────────────────────────────────────────────────────────
@cli.command()
@handle_error
def backends():
"""Show available execution backends and their status."""
runtime = get_runtime()
data = runtime.routing.describe()
if _json_output:
output(data)
else:
click.echo("Execution backends:\n")
for name, info in sorted(data.items(), key=lambda x: -x[1].get("priority", 0)):
status = "" if info.get("available") else ""
click.echo(
f" {status} {name:<20} priority={info.get('priority', '?'):<5}"
f" available={info.get('available')}"
)
# ── repl command ──────────────────────────────────────────────────────────────
@cli.command()
@click.pass_context
def repl(ctx):
"""Enter the interactive REPL (default when no command given)."""
global _repl_mode
_repl_mode = True
from cli_anything.openclaw.utils.repl_skin import ReplSkin
skin = ReplSkin("openclaw", version="1.0.0")
skin.print_banner()
runtime = get_runtime()
# Show quick summary on startup
macros = runtime.registry.list_all()
skin.info(f"{len(macros)} macros loaded. Type 'macro list' to see them.")
skin.info("Type 'help' for commands, 'quit' to exit.\n")
pt_session = skin.create_prompt_session()
session_obj = get_session()
while True:
try:
line = skin.get_input(
pt_session,
context=f"{session_obj.session_id[:12]}",
)
except (EOFError, KeyboardInterrupt):
skin.print_goodbye()
break
if not line:
continue
if line.lower() in ("quit", "exit", "q"):
skin.print_goodbye()
break
if line.lower() in ("help", "?"):
skin.help({
"macro list": "List all available macros",
"macro info <name>": "Show macro schema",
"macro run <name> [--param k=v ...]": "Execute a macro",
"macro dry-run <name>": "Simulate without side effects",
"macro validate [name]": "Validate macro definitions",
"macro define <name>": "Scaffold a new macro YAML",
"session status": "Show session statistics",
"session history": "Show recent runs",
"backends": "Show backend availability",
"quit": "Exit the REPL",
})
continue
# Parse and dispatch via Click's standalone_mode=False
import shlex
try:
args = shlex.split(line)
except ValueError as e:
skin.error(f"Parse error: {e}")
continue
try:
ctx_obj = cli.make_context(
"cli-anything-openclaw",
args,
standalone_mode=False,
parent=ctx,
)
with ctx_obj:
cli.invoke(ctx_obj)
except SystemExit:
pass
except click.ClickException as e:
skin.error(str(e))
except Exception as e:
skin.error(str(e))
# ── Entry point ───────────────────────────────────────────────────────────────
if __name__ == "__main__":
cli()
@@ -0,0 +1,220 @@
---
name: cli-anything-openclaw
description: >
Use when the agent wants to define, list, inspect, or execute GUI macros
via the OpenClaw Macro System CLI. Macros are parameterized, CLI-callable
workflows — the agent invokes `macro run <name>` and the system handles
backend routing (plugin, file transform, accessibility, compiled GUI replay).
---
# OpenClaw Macro System CLI
## What It Is
The OpenClaw Macro System converts valuable GUI workflows into parameterized,
CLI-callable macros. Agents **never touch the GUI directly** — they call macros
through this stable CLI, and the runtime routes execution to the best available
backend (native plugin/API, file transformation, semantic UI control, or
precompiled GUI macro replay).
## Installation
```bash
cd openclaw-skill/agent-harness
pip install -e .
```
**Requirements:** Python 3.10+, PyYAML, click, prompt-toolkit.
## Quick Start (for agents)
```bash
# 1. See what macros are available
cli-anything-openclaw macro list --json
# 2. Inspect a macro's parameters
cli-anything-openclaw macro info export_file --json
# 3. Dry-run to check params without side effects
cli-anything-openclaw --dry-run macro run export_file \
--param output=/tmp/test.txt --json
# 4. Execute a macro
cli-anything-openclaw macro run export_file \
--param output=/tmp/result.txt --json
# 5. See what backends are available
cli-anything-openclaw backends --json
```
## Command Reference
### Global Flags
| Flag | Description |
|------|-------------|
| `--json` | Machine-readable JSON output on stdout |
| `--dry-run` | Simulate all steps, skip side effects |
| `--session-id <id>` | Resume or create a named session |
### `macro` group
| Command | Description |
|---------|-------------|
| `macro list` | List all available macros |
| `macro info <name>` | Show macro schema (parameters, steps, conditions) |
| `macro run <name> --param k=v` | Execute a macro |
| `macro dry-run <name> --param k=v` | Simulate without side effects |
| `macro validate [name]` | Structural validation |
| `macro define <name>` | Scaffold a new macro YAML |
### `session` group
| Command | Description |
|---------|-------------|
| `session status` | Show session statistics |
| `session history` | Show recent run history |
| `session save` | Persist session to disk |
| `session list` | List all saved sessions |
### `backends`
```bash
cli-anything-openclaw backends --json
# Shows: native_api, file_transform, semantic_ui, gui_macro, recovery
# and whether each is available in the current environment.
```
## Macro Parameters
Pass parameters with `--param key=value`. Repeat for multiple:
```bash
cli-anything-openclaw macro run transform_json \
--param file=/path/to/data.json \
--param key=settings.theme \
--param value=dark \
--json
```
## Output Format (--json)
All commands output JSON when `--json` is set:
```json
{
"success": true,
"macro_name": "export_file",
"output": {
"exported_file": "/tmp/result.txt"
},
"error": "",
"telemetry": {
"duration_ms": 312,
"steps_total": 2,
"steps_run": 2,
"backends_used": ["native_api"],
"dry_run": false
}
}
```
On failure (`"success": false`), read the `"error"` field for the reason.
Exit code is 1 on failure.
## Execution Backends
Backends are selected automatically based on the macro step definition:
| Backend | Triggered by | Use case |
|---------|-------------|----------|
| `native_api` | `backend: native_api` | Subprocess / shell command |
| `file_transform` | `backend: file_transform` | XML, JSON, text file editing |
| `semantic_ui` | `backend: semantic_ui` | Accessibility / keyboard shortcuts |
| `gui_macro` | `backend: gui_macro` | Precompiled coordinate replay |
| `recovery` | `backend: recovery` | Retry / fallback orchestration |
## Writing Macros
Macros are YAML files in `cli_anything/openclaw/macro_definitions/`.
Scaffold one with:
```bash
cli-anything-openclaw macro define my_macro --output \
cli_anything/openclaw/macro_definitions/examples/my_macro.yaml
```
Minimal schema:
```yaml
name: my_macro
version: "1.0"
description: What this macro does.
parameters:
output:
type: string
required: true
description: Where to write results.
example: /tmp/result.txt
preconditions:
- file_exists: /path/to/input
steps:
- id: step1
backend: native_api
action: run_command
params:
command: [my-app, --export, "${output}"]
timeout_ms: 30000
on_failure: fail # or: skip, continue
postconditions:
- file_exists: ${output}
- file_size_gt: [${output}, 100]
outputs:
- name: result_file
path: ${output}
agent_hints:
danger_level: safe # safe | moderate | dangerous
side_effects: [creates_file]
reversible: true
```
## Agent Usage Rules
1. **Always use `--json`** for programmatic output.
2. **Use `--dry-run` to validate params** before executing side-effectful macros.
3. **Check `success` field** — do not assume success from exit code alone.
4. **Read `error` field** when `success` is false — it explains what failed.
5. **Use `macro info <name>` to discover params** before calling `macro run`.
6. **Use absolute paths** for all file parameters.
## Example Workflow
```bash
# Step 1: What's available?
cli-anything-openclaw macro list --json
# Step 2: What params does transform_json need?
cli-anything-openclaw macro info transform_json --json
# Step 3: Test safely
cli-anything-openclaw --dry-run macro run transform_json \
--param file=/tmp/config.json \
--param key=theme \
--param value=dark --json
# Step 4: Execute for real
cli-anything-openclaw macro run transform_json \
--param file=/tmp/config.json \
--param key=theme \
--param value=dark --json
```
## Version
1.0.0
@@ -0,0 +1,595 @@
"""Unit tests for OpenClaw Macro System core modules.
Covers: MacroDefinition, MacroRegistry, MacroRuntime, backends, routing.
All tests use synthetic data and do not require external software.
"""
import json
import os
import sys
import textwrap
import tempfile
from pathlib import Path
import pytest
# ── Helpers ───────────────────────────────────────────────────────────────────
SIMPLE_MACRO_YAML = textwrap.dedent("""\
name: test_macro
version: "1.0"
description: A test macro.
parameters:
output:
type: string
required: true
description: Output path
count:
type: integer
required: false
default: 1
min: 1
max: 100
steps:
- id: step1
backend: native_api
action: run_command
params:
command: [echo, hello]
postconditions: []
""")
def write_macro(tmp_path: Path, name: str, content: str) -> Path:
p = tmp_path / f"{name}.yaml"
p.write_text(content, encoding="utf-8")
return p
# ── macro_model tests ─────────────────────────────────────────────────────────
class TestMacroModel:
def test_load_from_yaml(self, tmp_path):
from cli_anything.openclaw.core.macro_model import load_from_yaml
p = write_macro(tmp_path, "test_macro", SIMPLE_MACRO_YAML)
m = load_from_yaml(str(p))
assert m.name == "test_macro"
assert m.version == "1.0"
assert "output" in m.parameters
assert len(m.steps) == 1
assert m.steps[0].backend == "native_api"
def test_load_missing_file(self):
from cli_anything.openclaw.core.macro_model import load_from_yaml
with pytest.raises(FileNotFoundError):
load_from_yaml("/nonexistent/path.yaml")
def test_validate_params_required(self, tmp_path):
from cli_anything.openclaw.core.macro_model import load_from_yaml
p = write_macro(tmp_path, "test_macro", SIMPLE_MACRO_YAML)
m = load_from_yaml(str(p))
errors = m.validate_params({})
assert any("output" in e for e in errors)
def test_validate_params_type_error(self, tmp_path):
from cli_anything.openclaw.core.macro_model import load_from_yaml
p = write_macro(tmp_path, "test_macro", SIMPLE_MACRO_YAML)
m = load_from_yaml(str(p))
errors = m.validate_params({"output": "/tmp/x", "count": "not_an_int"})
assert any("count" in e for e in errors)
def test_validate_params_range(self, tmp_path):
from cli_anything.openclaw.core.macro_model import load_from_yaml
p = write_macro(tmp_path, "test_macro", SIMPLE_MACRO_YAML)
m = load_from_yaml(str(p))
errors = m.validate_params({"output": "/tmp/x", "count": 200})
assert any("count" in e for e in errors)
def test_validate_params_ok(self, tmp_path):
from cli_anything.openclaw.core.macro_model import load_from_yaml
p = write_macro(tmp_path, "test_macro", SIMPLE_MACRO_YAML)
m = load_from_yaml(str(p))
errors = m.validate_params({"output": "/tmp/x"})
assert errors == []
def test_resolve_params_defaults(self, tmp_path):
from cli_anything.openclaw.core.macro_model import load_from_yaml
p = write_macro(tmp_path, "test_macro", SIMPLE_MACRO_YAML)
m = load_from_yaml(str(p))
resolved = m.resolve_params({"output": "/tmp/x"})
assert resolved["count"] == 1
def test_structural_validation_no_steps(self, tmp_path):
from cli_anything.openclaw.core.macro_model import load_from_yaml
yaml_content = "name: bad\nsteps: []\n"
p = write_macro(tmp_path, "bad", yaml_content)
m = load_from_yaml(str(p))
errors = m.validate()
assert any("steps" in e for e in errors)
def test_structural_validation_bad_backend(self, tmp_path):
from cli_anything.openclaw.core.macro_model import load_from_yaml
yaml_content = textwrap.dedent("""\
name: bad
steps:
- id: x
backend: fake_backend
action: do_thing
""")
p = write_macro(tmp_path, "bad_backend", yaml_content)
m = load_from_yaml(str(p))
errors = m.validate()
assert any("fake_backend" in e for e in errors)
def test_to_dict(self, tmp_path):
from cli_anything.openclaw.core.macro_model import load_from_yaml
p = write_macro(tmp_path, "test_macro", SIMPLE_MACRO_YAML)
m = load_from_yaml(str(p))
d = m.to_dict()
assert d["name"] == "test_macro"
assert "parameters" in d
assert "steps" in d
class TestSubstitute:
def test_string_substitution(self):
from cli_anything.openclaw.core.macro_model import substitute
result = substitute("hello ${name}", {"name": "world"})
assert result == "hello world"
def test_nested_list(self):
from cli_anything.openclaw.core.macro_model import substitute
result = substitute(["echo", "${output}"], {"output": "/tmp/x"})
assert result == ["echo", "/tmp/x"]
def test_nested_dict(self):
from cli_anything.openclaw.core.macro_model import substitute
result = substitute({"path": "${output}", "other": 42}, {"output": "/out"})
assert result["path"] == "/out"
assert result["other"] == 42
def test_missing_key_left_as_is(self):
from cli_anything.openclaw.core.macro_model import substitute
result = substitute("${missing}", {})
assert result == "${missing}"
# ── MacroRegistry tests ───────────────────────────────────────────────────────
class TestMacroRegistry:
def test_load_macro(self, tmp_path):
from cli_anything.openclaw.core.registry import MacroRegistry
write_macro(tmp_path, "test_macro", SIMPLE_MACRO_YAML)
reg = MacroRegistry(str(tmp_path))
m = reg.load("test_macro")
assert m.name == "test_macro"
def test_load_missing_raises(self, tmp_path):
from cli_anything.openclaw.core.registry import MacroRegistry
reg = MacroRegistry(str(tmp_path))
with pytest.raises(KeyError):
reg.load("nonexistent_macro")
def test_list_all(self, tmp_path):
from cli_anything.openclaw.core.registry import MacroRegistry
write_macro(tmp_path, "test_macro", SIMPLE_MACRO_YAML)
write_macro(tmp_path, "another", SIMPLE_MACRO_YAML.replace("test_macro", "another"))
reg = MacroRegistry(str(tmp_path))
names = reg.list_names()
assert "test_macro" in names
assert "another" in names
def test_manifest_index(self, tmp_path):
from cli_anything.openclaw.core.registry import MacroRegistry
sub = tmp_path / "sub"
sub.mkdir()
write_macro(sub, "alpha", SIMPLE_MACRO_YAML.replace("test_macro", "alpha"))
manifest = tmp_path / "manifest.yaml"
manifest.write_text("macros:\n - name: alpha\n path: sub/alpha.yaml\n")
reg = MacroRegistry(str(tmp_path))
m = reg.load("alpha")
assert m.name == "alpha"
def test_register_programmatic(self):
from cli_anything.openclaw.core.registry import MacroRegistry
from cli_anything.openclaw.core.macro_model import MacroDefinition, MacroStep
reg = MacroRegistry("/nonexistent")
macro = MacroDefinition(
name="inline_macro",
steps=[MacroStep(backend="native_api", action="run_command")],
)
reg.register(macro)
assert reg.load("inline_macro").name == "inline_macro"
def test_info(self, tmp_path):
from cli_anything.openclaw.core.registry import MacroRegistry
write_macro(tmp_path, "test_macro", SIMPLE_MACRO_YAML)
reg = MacroRegistry(str(tmp_path))
info = reg.info()
assert info["total"] >= 1
assert "macros_dir" in info
# ── Backend tests ─────────────────────────────────────────────────────────────
class TestNativeAPIBackend:
def _make_context(self, params=None):
from cli_anything.openclaw.backends.base import BackendContext
return BackendContext(params=params or {})
def _make_step(self, action, step_params):
from cli_anything.openclaw.core.macro_model import MacroStep
return MacroStep(id="test", backend="native_api", action=action, params=step_params)
def test_run_command_success(self):
from cli_anything.openclaw.backends.native_api import NativeAPIBackend
b = NativeAPIBackend()
step = self._make_step("run_command", {"command": ["echo", "hello"]})
result = b.execute(step, {}, self._make_context())
assert result.success
def test_run_command_fails_bad_exit(self):
from cli_anything.openclaw.backends.native_api import NativeAPIBackend
b = NativeAPIBackend()
step = self._make_step("run_command", {"command": ["false"]})
result = b.execute(step, {}, self._make_context())
assert not result.success
assert "exit" in result.error.lower() or "failed" in result.error.lower()
def test_run_command_not_found(self):
from cli_anything.openclaw.backends.native_api import NativeAPIBackend
b = NativeAPIBackend()
step = self._make_step("run_command", {"command": ["__nonexistent_cmd__"]})
result = b.execute(step, {}, self._make_context())
assert not result.success
def test_find_executable_found(self):
from cli_anything.openclaw.backends.native_api import NativeAPIBackend
b = NativeAPIBackend()
step = self._make_step("find_executable", {"name": "echo"})
result = b.execute(step, {}, self._make_context())
assert result.success
assert "executable" in result.output
def test_find_executable_missing(self):
from cli_anything.openclaw.backends.native_api import NativeAPIBackend
b = NativeAPIBackend()
step = self._make_step("find_executable", {
"name": "__nonexistent__",
"install_hint": "brew install nonexistent"
})
result = b.execute(step, {}, self._make_context())
assert not result.success
assert "brew install" in result.error
def test_dry_run_does_not_execute(self):
from cli_anything.openclaw.backends.native_api import NativeAPIBackend
from cli_anything.openclaw.backends.base import BackendContext
b = NativeAPIBackend()
step = self._make_step("run_command", {"command": ["false"]})
ctx = BackendContext(params={}, dry_run=True)
result = b.execute(step, {}, ctx)
assert result.success
assert result.output.get("dry_run")
def test_param_substitution_in_command(self):
from cli_anything.openclaw.backends.native_api import NativeAPIBackend
b = NativeAPIBackend()
step = self._make_step("run_command", {"command": ["echo", "${msg}"]})
result = b.execute(step, {"msg": "substituted"}, self._make_context({"msg": "substituted"}))
assert result.success
def test_unknown_action(self):
from cli_anything.openclaw.backends.native_api import NativeAPIBackend
b = NativeAPIBackend()
step = self._make_step("unknown_action", {})
result = b.execute(step, {}, self._make_context())
assert not result.success
assert "unknown action" in result.error.lower()
class TestFileTransformBackend:
def _make_context(self):
from cli_anything.openclaw.backends.base import BackendContext
return BackendContext(params={})
def test_json_set_and_get(self, tmp_path):
from cli_anything.openclaw.backends.file_transform import FileTransformBackend
from cli_anything.openclaw.core.macro_model import MacroStep
b = FileTransformBackend()
ctx = self._make_context()
json_file = tmp_path / "data.json"
json_file.write_text('{"a": 1}', encoding="utf-8")
step = MacroStep(id="set", backend="file_transform", action="json_set", params={
"input_file": str(json_file),
"output_file": str(json_file),
"path": "settings.theme",
"value": "dark",
})
result = b.execute(step, {}, ctx)
assert result.success
import json
data = json.loads(json_file.read_text())
assert data["settings"]["theme"] == "dark"
def test_text_replace(self, tmp_path):
from cli_anything.openclaw.backends.file_transform import FileTransformBackend
from cli_anything.openclaw.core.macro_model import MacroStep
b = FileTransformBackend()
ctx = self._make_context()
txt_file = tmp_path / "config.ini"
txt_file.write_text("theme=default\nsize=10\n", encoding="utf-8")
step = MacroStep(id="replace", backend="file_transform", action="text_replace", params={
"input_file": str(txt_file),
"output_file": str(txt_file),
"find": "theme=default",
"replace": "theme=dark",
})
result = b.execute(step, {}, ctx)
assert result.success
assert "theme=dark" in txt_file.read_text()
assert result.output["replacements"] == 1
def test_copy_file(self, tmp_path):
from cli_anything.openclaw.backends.file_transform import FileTransformBackend
from cli_anything.openclaw.core.macro_model import MacroStep
b = FileTransformBackend()
ctx = self._make_context()
src = tmp_path / "src.txt"
dst = tmp_path / "dst.txt"
src.write_text("content", encoding="utf-8")
step = MacroStep(id="copy", backend="file_transform", action="copy_file", params={
"src": str(src),
"dst": str(dst),
})
result = b.execute(step, {}, ctx)
assert result.success
assert dst.read_text() == "content"
def test_unknown_action(self):
from cli_anything.openclaw.backends.file_transform import FileTransformBackend
from cli_anything.openclaw.core.macro_model import MacroStep
b = FileTransformBackend()
step = MacroStep(id="x", backend="file_transform", action="unknown_op", params={})
result = b.execute(step, {}, self._make_context())
assert not result.success
class TestStepResult:
def test_to_dict(self):
from cli_anything.openclaw.backends.base import StepResult
r = StepResult(success=True, output={"key": "val"}, backend_used="native_api")
d = r.to_dict()
assert d["success"] is True
assert d["output"]["key"] == "val"
assert d["backend_used"] == "native_api"
# ── Routing tests ─────────────────────────────────────────────────────────────
class TestRoutingEngine:
def test_select_native_api(self):
from cli_anything.openclaw.core.routing import RoutingEngine
from cli_anything.openclaw.core.macro_model import MacroStep
engine = RoutingEngine()
step = MacroStep(id="x", backend="native_api", action="run_command")
backend = engine.select(step)
assert backend.name == "native_api"
def test_select_file_transform(self):
from cli_anything.openclaw.core.routing import RoutingEngine
from cli_anything.openclaw.core.macro_model import MacroStep
engine = RoutingEngine()
step = MacroStep(id="x", backend="file_transform", action="json_set")
backend = engine.select(step)
assert backend.name == "file_transform"
def test_describe(self):
from cli_anything.openclaw.core.routing import RoutingEngine
engine = RoutingEngine()
desc = engine.describe()
assert "native_api" in desc
assert "file_transform" in desc
assert "recovery" in desc
def test_execute_step_native_api(self):
from cli_anything.openclaw.core.routing import RoutingEngine
from cli_anything.openclaw.core.macro_model import MacroStep
from cli_anything.openclaw.backends.base import BackendContext
engine = RoutingEngine()
step = MacroStep(id="x", backend="native_api", action="run_command",
params={"command": ["echo", "hello"]})
ctx = BackendContext(params={})
result = engine.execute_step(step, {}, ctx)
assert result.success
# ── Runtime tests ─────────────────────────────────────────────────────────────
class TestMacroRuntime:
def _make_runtime(self, tmp_path):
from cli_anything.openclaw.core.registry import MacroRegistry
from cli_anything.openclaw.core.runtime import MacroRuntime
# Register a real macro that just echoes
yaml_content = textwrap.dedent("""\
name: echo_macro
parameters:
msg:
type: string
required: false
default: hello
steps:
- id: step1
backend: native_api
action: run_command
params:
command: [echo, "${msg}"]
""")
write_macro(tmp_path, "echo_macro", yaml_content)
reg = MacroRegistry(str(tmp_path))
return MacroRuntime(registry=reg)
def test_execute_success(self, tmp_path):
rt = self._make_runtime(tmp_path)
result = rt.execute("echo_macro", {"msg": "test"})
assert result.success
assert result.telemetry["steps_run"] == 1
def test_execute_unknown_macro(self, tmp_path):
rt = self._make_runtime(tmp_path)
result = rt.execute("nonexistent_macro", {})
assert not result.success
assert "not found" in result.error.lower()
def test_execute_param_validation_failure(self, tmp_path):
from cli_anything.openclaw.core.registry import MacroRegistry
from cli_anything.openclaw.core.runtime import MacroRuntime
yaml_content = textwrap.dedent("""\
name: required_param_macro
parameters:
output:
type: string
required: true
steps:
- id: s1
backend: native_api
action: run_command
params:
command: [echo, "${output}"]
""")
write_macro(tmp_path, "required_param_macro", yaml_content)
reg = MacroRegistry(str(tmp_path))
rt = MacroRuntime(registry=reg)
result = rt.execute("required_param_macro", {})
assert not result.success
assert "output" in result.error
def test_precondition_failure(self, tmp_path):
from cli_anything.openclaw.core.registry import MacroRegistry
from cli_anything.openclaw.core.runtime import MacroRuntime
yaml_content = textwrap.dedent("""\
name: precond_macro
preconditions:
- file_exists: /nonexistent_file_xyz_abc
steps:
- id: s1
backend: native_api
action: run_command
params:
command: [echo, ok]
""")
write_macro(tmp_path, "precond_macro", yaml_content)
reg = MacroRegistry(str(tmp_path))
rt = MacroRuntime(registry=reg)
result = rt.execute("precond_macro", {})
assert not result.success
assert "precondition" in result.error.lower()
def test_postcondition_failure(self, tmp_path):
from cli_anything.openclaw.core.registry import MacroRegistry
from cli_anything.openclaw.core.runtime import MacroRuntime
yaml_content = textwrap.dedent("""\
name: postcond_macro
steps:
- id: s1
backend: native_api
action: run_command
params:
command: [echo, ok]
postconditions:
- file_exists: /nonexistent_output_xyz
""")
write_macro(tmp_path, "postcond_macro", yaml_content)
reg = MacroRegistry(str(tmp_path))
rt = MacroRuntime(registry=reg)
result = rt.execute("postcond_macro", {})
assert not result.success
assert "postcondition" in result.error.lower()
def test_dry_run(self, tmp_path):
rt = self._make_runtime(tmp_path)
result = rt.execute("echo_macro", {}, dry_run=True)
assert result.success
assert result.telemetry["dry_run"] is True
def test_session_records_run(self, tmp_path):
rt = self._make_runtime(tmp_path)
rt.execute("echo_macro", {})
last = rt.session.last()
assert last is not None
assert last.macro_name == "echo_macro"
def test_validate_macro(self, tmp_path):
rt = self._make_runtime(tmp_path)
errors = rt.validate_macro("echo_macro")
assert errors == []
def test_on_failure_skip(self, tmp_path):
from cli_anything.openclaw.core.registry import MacroRegistry
from cli_anything.openclaw.core.runtime import MacroRuntime
yaml_content = textwrap.dedent("""\
name: skip_macro
steps:
- id: bad_step
backend: native_api
action: run_command
params:
command: [false]
on_failure: skip
- id: good_step
backend: native_api
action: run_command
params:
command: [echo, reached]
""")
write_macro(tmp_path, "skip_macro", yaml_content)
reg = MacroRegistry(str(tmp_path))
rt = MacroRuntime(registry=reg)
result = rt.execute("skip_macro", {})
# Should succeed because bad step was skipped
assert result.success
assert result.telemetry["steps_run"] == 2
# ── Session tests ─────────────────────────────────────────────────────────────
class TestExecutionSession:
def test_record_and_retrieve(self):
from cli_anything.openclaw.core.session import ExecutionSession, RunRecord
sess = ExecutionSession(session_id="test_sess")
rec = RunRecord("m1", {}, True, {}, "", 100.0, ["native_api"], 1)
sess.record(rec)
assert sess.last().macro_name == "m1"
assert len(sess.history()) == 1
def test_stats(self):
from cli_anything.openclaw.core.session import ExecutionSession, RunRecord
sess = ExecutionSession()
sess.record(RunRecord("m1", {}, True, {}, "", 100.0, [], 1))
sess.record(RunRecord("m2", {}, False, {}, "err", 50.0, [], 0))
stats = sess.stats()
assert stats["total"] == 2
assert stats["success"] == 1
assert stats["success_rate"] == 0.5
def test_save_and_load(self, tmp_path, monkeypatch):
from cli_anything.openclaw.core import session as sess_mod
import cli_anything.openclaw.core.session as sess_module
# Redirect SESSION_DIR to tmp_path
monkeypatch.setattr(sess_module, "SESSION_DIR", tmp_path)
from cli_anything.openclaw.core.session import ExecutionSession, RunRecord
sess = ExecutionSession(session_id="save_test")
sess.record(RunRecord("m1", {"k": "v"}, True, {}, "", 200.0, ["native_api"], 1))
sess.save()
loaded = ExecutionSession.load("save_test")
assert loaded is not None
assert loaded.session_id == "save_test"
assert loaded.last().macro_name == "m1"
@@ -0,0 +1,369 @@
"""End-to-end integration tests for the OpenClaw Macro System.
Tests the full lifecycle: macro discovery runtime execution CLI subprocess.
Uses real file I/O and subprocess calls (echo, cat, etc.) as the "target apps".
"""
import json
import os
import subprocess
import sys
import tempfile
import textwrap
from pathlib import Path
import pytest
# ── CLI resolver (same pattern as in HARNESS.md) ─────────────────────────────
def _resolve_cli(name: str) -> list[str]:
"""Resolve installed CLI command; falls back to python -m for dev."""
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 = "cli_anything.openclaw.openclaw_cli"
print(f"[_resolve_cli] Falling back to: {sys.executable} -m {module}")
return [sys.executable, "-m", "cli_anything.openclaw"]
# ── Helpers ───────────────────────────────────────────────────────────────────
def write_macro(tmp_path: Path, name: str, content: str) -> Path:
p = tmp_path / f"{name}.yaml"
p.write_text(content, encoding="utf-8")
return p
# ── E2E: File transform workflow ──────────────────────────────────────────────
class TestFileTransformE2E:
def test_json_set_and_verify(self, tmp_path):
"""Write a JSON file, transform it, verify the result."""
from cli_anything.openclaw.core.macro_model import MacroDefinition, MacroStep, MacroCondition, MacroOutput
from cli_anything.openclaw.core.registry import MacroRegistry
from cli_anything.openclaw.core.runtime import MacroRuntime
json_file = tmp_path / "settings.json"
json_file.write_text('{"version": 1}', encoding="utf-8")
yaml_content = textwrap.dedent(f"""\
name: set_json_key
parameters:
file:
type: string
required: true
key:
type: string
required: true
value:
type: string
required: true
preconditions:
- file_exists: ${{file}}
steps:
- id: transform
backend: file_transform
action: json_set
params:
input_file: ${{file}}
output_file: ${{file}}
path: ${{key}}
value: ${{value}}
postconditions:
- file_exists: ${{file}}
outputs:
- name: modified_file
path: ${{file}}
""")
write_macro(tmp_path, "set_json_key", yaml_content)
reg = MacroRegistry(str(tmp_path))
rt = MacroRuntime(registry=reg)
result = rt.execute("set_json_key", {
"file": str(json_file),
"key": "config.theme",
"value": "dark",
})
assert result.success, f"Failed: {result.error}"
data = json.loads(json_file.read_text())
assert data["config"]["theme"] == "dark"
print(f"\n Modified JSON: {json_file}{json.dumps(data)}")
class TestNativeAPIE2E:
def test_run_real_command_and_capture(self, tmp_path):
"""Run a real shell command and capture its stdout."""
from cli_anything.openclaw.core.macro_model import MacroDefinition
from cli_anything.openclaw.core.registry import MacroRegistry
from cli_anything.openclaw.core.runtime import MacroRuntime
output_file = tmp_path / "result.txt"
yaml_content = textwrap.dedent(f"""\
name: capture_date
parameters:
output:
type: string
required: true
steps:
- id: run_date
backend: native_api
action: run_command
params:
command: [date, "+%Y-%m-%d"]
capture_stdout: true
postconditions: []
outputs:
- name: output_file
path: ${{output}}
""")
write_macro(tmp_path, "capture_date", yaml_content)
reg = MacroRegistry(str(tmp_path))
rt = MacroRuntime(registry=reg)
result = rt.execute("capture_date", {"output": str(output_file)})
assert result.success, f"Failed: {result.error}"
# Step output contains stdout
steps_output = result.output.get("_steps", [])
assert steps_output, "Expected _steps in output"
stdout = steps_output[0].get("stdout", "")
# Date format YYYY-MM-DD
import re
assert re.match(r"\d{4}-\d{2}-\d{2}", stdout.strip()), f"Unexpected stdout: {stdout!r}"
print(f"\n Captured date: {stdout.strip()}")
def test_step_failure_aborts_macro(self, tmp_path):
"""A failing step with on_failure=fail should abort the macro."""
from cli_anything.openclaw.core.registry import MacroRegistry
from cli_anything.openclaw.core.runtime import MacroRuntime
yaml_content = textwrap.dedent("""\
name: fail_abort
steps:
- id: bad
backend: native_api
action: run_command
params:
command: [false]
on_failure: fail
- id: good
backend: native_api
action: run_command
params:
command: [echo, should_not_run]
""")
write_macro(tmp_path, "fail_abort", yaml_content)
reg = MacroRegistry(str(tmp_path))
rt = MacroRuntime(registry=reg)
result = rt.execute("fail_abort", {})
assert not result.success
# Only the first step should have run
assert result.telemetry["steps_run"] == 1
print(f"\n Correctly aborted after first step: {result.error}")
def test_step_failure_skip_continues(self, tmp_path):
"""A failing step with on_failure=skip should allow the macro to continue."""
from cli_anything.openclaw.core.registry import MacroRegistry
from cli_anything.openclaw.core.runtime import MacroRuntime
yaml_content = textwrap.dedent("""\
name: fail_skip
steps:
- id: bad
backend: native_api
action: run_command
params:
command: [false]
on_failure: skip
- id: good
backend: native_api
action: run_command
params:
command: [echo, still_ran]
""")
write_macro(tmp_path, "fail_skip", yaml_content)
reg = MacroRegistry(str(tmp_path))
rt = MacroRuntime(registry=reg)
result = rt.execute("fail_skip", {})
assert result.success
assert result.telemetry["steps_run"] == 2
print(f"\n Macro succeeded despite skipped step")
class TestPostconditionE2E:
def test_postcondition_file_exists_passes(self, tmp_path):
from cli_anything.openclaw.core.registry import MacroRegistry
from cli_anything.openclaw.core.runtime import MacroRuntime
output_file = tmp_path / "output.txt"
yaml_content = textwrap.dedent(f"""\
name: write_and_verify
parameters:
output:
type: string
required: true
steps:
- id: write
backend: file_transform
action: text_replace
params:
input_file: /dev/null
output_file: ${{output}}
find: ""
replace: ""
postconditions:
- file_exists: ${{output}}
""")
write_macro(tmp_path, "write_and_verify", yaml_content)
reg = MacroRegistry(str(tmp_path))
rt = MacroRuntime(registry=reg)
result = rt.execute("write_and_verify", {"output": str(output_file)})
assert result.success, f"Failed: {result.error}"
print(f"\n Output file: {output_file} ({output_file.stat().st_size} bytes)")
def test_postcondition_file_size_gt(self, tmp_path):
from cli_anything.openclaw.core.registry import MacroRegistry
from cli_anything.openclaw.core.runtime import MacroRuntime
output_file = tmp_path / "output.txt"
output_file.write_text("x" * 200, encoding="utf-8")
yaml_content = textwrap.dedent(f"""\
name: size_check
parameters:
output:
type: string
required: true
steps:
- id: noop
backend: native_api
action: run_command
params:
command: [echo, noop]
postconditions:
- file_size_gt:
- ${{output}}
- 100
""")
write_macro(tmp_path, "size_check", yaml_content)
reg = MacroRegistry(str(tmp_path))
rt = MacroRuntime(registry=reg)
result = rt.execute("size_check", {"output": str(output_file)})
assert result.success, f"Failed: {result.error}"
print(f"\n File size: {output_file.stat().st_size} bytes")
# ── CLI subprocess tests ──────────────────────────────────────────────────────
class TestCLISubprocess:
CLI_BASE = _resolve_cli("cli-anything-openclaw")
def _run(self, args: list[str], check: bool = True) -> subprocess.CompletedProcess:
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
assert "macro" in result.stdout.lower()
def test_macro_list_json(self):
result = self._run(["--json", "macro", "list"])
assert result.returncode == 0
data = json.loads(result.stdout)
assert isinstance(data, list)
print(f"\n macros: {[m['name'] for m in data]}")
def test_macro_info_json(self):
# Info on a bundled example macro
result = self._run(["--json", "macro", "info", "export_file"])
assert result.returncode == 0, f"stderr: {result.stderr}"
data = json.loads(result.stdout)
assert data["name"] == "export_file"
assert "parameters" in data
print(f"\n Macro info: {data['name']} v{data['version']}")
def test_macro_validate_all(self):
result = self._run(["--json", "macro", "validate"])
assert result.returncode == 0
data = json.loads(result.stdout)
assert isinstance(data, dict)
for name, info in data.items():
assert info["valid"], f"Macro {name} failed: {info['errors']}"
print(f"\n Validated {len(data)} macros, all valid")
def test_macro_dry_run_json(self):
result = self._run([
"--json", "--dry-run",
"macro", "run", "export_file",
"--param", "output=/tmp/test_openclaw_e2e.txt",
])
assert result.returncode == 0, f"stderr: {result.stderr}"
data = json.loads(result.stdout)
assert data["success"] is True
assert data["telemetry"]["dry_run"] is True
print(f"\n Dry run result: {data['success']}")
def test_backends_json(self):
result = self._run(["--json", "backends"])
assert result.returncode == 0
data = json.loads(result.stdout)
assert "native_api" in data
assert "file_transform" in data
print(f"\n Backends: {list(data.keys())}")
def test_session_status_json(self):
result = self._run(["--json", "session", "status"])
assert result.returncode == 0
data = json.loads(result.stdout)
assert "session_id" in data
print(f"\n Session: {data['session_id']}")
def test_macro_run_json_transform_workflow(self, tmp_path):
"""Full E2E: create a JSON file, run transform_json macro, verify output."""
json_file = tmp_path / "data.json"
json_file.write_text('{"name": "test"}', encoding="utf-8")
result = self._run([
"--json",
"macro", "run", "transform_json",
"--param", f"file={json_file}",
"--param", "key=config.mode",
"--param", "value=production",
])
print(f"\n CLI stdout: {result.stdout[:200]}")
print(f" CLI stderr: {result.stderr[:200]}")
assert result.returncode == 0, f"CLI failed:\n{result.stderr}"
data = json.loads(result.stdout)
assert data["success"] is True, f"Macro failed: {data.get('error')}"
# Verify file was actually modified
modified = json.loads(json_file.read_text())
assert modified["config"]["mode"] == "production"
print(f"\n Modified JSON: {modified}")
print(f" File: {json_file} ({json_file.stat().st_size} bytes)")
def test_unknown_macro_returns_error_json(self):
result = self._run(
["--json", "macro", "run", "nonexistent_macro_xyz"],
check=False,
)
assert result.returncode != 0
data = json.loads(result.stdout)
assert data["success"] is False
assert "error" in data
print(f"\n Error: {data['error']}")
@@ -0,0 +1,521 @@
"""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
}
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
"""
setup.py for cli-anything-openclaw
Install with: pip install -e .
"""
from setuptools import setup, find_namespace_packages
with open("cli_anything/openclaw/README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
setup(
name="cli-anything-openclaw",
version="1.0.0",
author="cli-anything contributors",
author_email="",
description=(
"OpenClaw Macro System — A layered CLI that converts GUI workflows into "
"parameterized, agent-callable macros. Requires: PyYAML, click, prompt-toolkit."
),
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/HKUDS/CLI-Anything",
packages=find_namespace_packages(include=["cli_anything.*"]),
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Topic :: Software Development :: Libraries :: Python Modules",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
],
python_requires=">=3.10",
install_requires=[
"click>=8.0.0",
"prompt-toolkit>=3.0.0",
"PyYAML>=6.0",
],
extras_require={
"dev": [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
],
},
entry_points={
"console_scripts": [
"cli-anything-openclaw=cli_anything.openclaw.openclaw_cli:cli",
],
},
package_data={
"cli_anything.openclaw": ["skills/*.md", "macro_definitions/*.yaml", "macro_definitions/examples/*.yaml"],
},
include_package_data=True,
zip_safe=False,
)
@@ -7,7 +7,7 @@ Usage:
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("shotcut", version="1.0.0")
skin.print_banner()
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -57,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -89,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -97,7 +111,7 @@ class ReplSkin:
"""
def __init__(self, software: str, version: str = "1.0.0",
history_file: str | None = None):
history_file: str | None = None, skill_path: str | None = None):
"""Initialize the REPL skin.
Args:
@@ -105,15 +119,45 @@ class ReplSkin:
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 repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
if skill_path is None:
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -143,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -151,6 +197,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -165,9 +229,14 @@ class ReplSkin:
tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}"
empty = ""
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -459,26 +528,6 @@ class ReplSkin:
raw_prompt = self.prompt(project_name, modified, context)
return input(raw_prompt).strip()
# ── Sub-prompt input ────────────────────────────────────────────
def sub_input(self, prompt_text: str, pt_session=None) -> str:
"""Get input for a sub-prompt (e.g., parameter entry in add flows).
Uses prompt_toolkit if a session is available, otherwise falls back
to plain input(). This preserves history and styling consistency.
Args:
prompt_text: The prompt to display (e.g., " start_ms: ").
pt_session: An optional prompt_toolkit PromptSession.
Returns:
User input string (stripped).
"""
if pt_session is not None:
return pt_session.prompt(prompt_text).strip()
else:
return input(prompt_text).strip()
# ── Toolbar builder ───────────────────────────────────────────────
def bottom_toolbar(self, items: dict[str, str]):
@@ -6,20 +6,21 @@ Copy this file into your CLI package at:
Usage:
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("ollama", version="1.0.0")
skin.print_banner()
prompt_text = skin.prompt(project_name="llama3.2", modified=False)
skin.success("Model pulled")
skin.error("Connection failed")
skin.warning("No models loaded")
skin.info("Generating...")
skin.status("Model", "llama3.2:latest")
skin = ReplSkin("shotcut", version="1.0.0")
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
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
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -47,7 +48,6 @@ _ACCENT_COLORS = {
"obs_studio": "\033[38;5;55m", # purple
"kdenlive": "\033[38;5;69m", # slate blue
"shotcut": "\033[38;5;35m", # teal green
"ollama": "\033[38;5;255m", # white (Ollama branding)
}
_DEFAULT_ACCENT = "\033[38;5;75m" # default sky blue
@@ -58,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -90,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -98,23 +111,53 @@ class ReplSkin:
"""
def __init__(self, software: str, version: str = "1.0.0",
history_file: str | None = None):
history_file: str | None = None, skill_path: str | None = None):
"""Initialize the REPL skin.
Args:
software: Software name (e.g., "gimp", "shotcut", "ollama").
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 repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
if skill_path is None:
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -144,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -152,10 +197,28 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
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 · Ollama
# Title: ◆ cli-anything · Shotcut
icon = self._c(_CYAN + _BOLD, "")
brand = self._c(_CYAN + _BOLD, "cli-anything")
dot = self._c(_DARK_GRAY, "·")
@@ -166,9 +229,14 @@ class ReplSkin:
tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}"
empty = ""
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -496,5 +564,4 @@ _ANSI_256_TO_HEX = {
"\033[38;5;80m": "#5fd7d7", # brand cyan
"\033[38;5;208m": "#ff8700", # blender deep orange
"\033[38;5;214m": "#ffaf00", # gimp warm orange
"\033[38;5;255m": "#eeeeee", # ollama white
}
+102 -1
View File
@@ -2,7 +2,7 @@
"meta": {
"repo": "https://github.com/HKUDS/CLI-Anything",
"description": "Public CLI Registry — Third-party and official CLIs managed by CLI-Hub across npm, bundled, brew, and other install methods",
"updated": "2026-04-16"
"updated": "2026-04-18"
},
"clis": [
{
@@ -18,6 +18,7 @@
"npm_package": "@larksuite/cli",
"install_cmd": "npm install -g @larksuite/cli",
"npx_cmd": "npx @larksuite/cli",
"skill_md": "npx skills add larksuite/cli -y -g",
"entry_point": "lark-cli",
"contributors": [
{
@@ -39,6 +40,7 @@
"npm_package": "minimax-cli",
"install_cmd": "npm install -g minimax-cli",
"npx_cmd": "npx minimax-cli",
"skill_md": "https://platform.minimax.io/docs/token-plan/minimax-cli",
"entry_point": "minimax-cli",
"contributors": [
{
@@ -60,6 +62,7 @@
"npm_package": "@wecom/cli",
"install_cmd": "npm install -g @wecom/cli",
"npx_cmd": "npx @wecom/cli",
"skill_md": "npx skills add WeComTeam/wecom-cli -y -g",
"entry_point": "wecom-cli",
"contributors": [
{
@@ -81,6 +84,7 @@
"npm_package": "contentful-cli",
"install_cmd": "npm install -g contentful-cli",
"npx_cmd": "npx contentful-cli",
"skill_md": "https://github.com/contentful/contentful-cli/tree/main/docs",
"entry_point": "contentful",
"contributors": [
{
@@ -102,6 +106,7 @@
"npm_package": "sanity",
"install_cmd": "npm install -g sanity",
"npx_cmd": "npx sanity@latest",
"skill_md": "https://www.sanity.io/docs/apis-and-sdks/cli",
"entry_point": "sanity",
"contributors": [
{
@@ -123,6 +128,7 @@
"npm_package": "@shopify/cli",
"install_cmd": "npm install -g @shopify/cli",
"npx_cmd": "npx @shopify/cli@latest",
"skill_md": "https://github.com/Shopify/cli/blob/main/packages/cli/README.md#commands",
"entry_point": "shopify",
"contributors": [
{
@@ -144,6 +150,7 @@
"npm_package": "@sentry/cli",
"install_cmd": "npm install -g @sentry/cli",
"npx_cmd": "npx @sentry/cli",
"skill_md": "https://docs.sentry.io/cli/",
"entry_point": "sentry-cli",
"contributors": [
{
@@ -166,6 +173,7 @@
"install_cmd": "brew install --cask 1password-cli",
"uninstall_cmd": "brew uninstall --cask 1password-cli",
"update_cmd": "brew upgrade --cask 1password-cli",
"skill_md": "https://developer.1password.com/docs/cli/get-started/",
"entry_point": "op",
"contributors": [
{
@@ -174,6 +182,31 @@
}
]
},
{
"name": "android-cli",
"display_name": "Android CLI",
"version": "0.7",
"description": "Official Android terminal interface for SDK setup, project creation, emulator/device management, app run/deploy workflows, docs access, and skill management for any agent",
"category": "mobile",
"requires": "Preview release; Linux x86_64, macOS arm64, or Windows x86_64. Android SDK / JDK requirements depend on the workflow you run.",
"homepage": "https://developer.android.com/tools/agents/android-cli",
"docs_url": "https://android-developers.googleblog.com/2026/04/build-android-apps-3x-faster-using-any-agent.html",
"source_url": null,
"package_manager": "bundled",
"install_strategy": "bundled",
"install_notes": "Install Android CLI from the official archive page: Linux `curl -fsSL https://dl.google.com/android/cli/latest/linux_x86_64/install.sh | bash`; macOS `curl -fsSL https://dl.google.com/android/cli/latest/darwin_arm64/install.sh | bash`; Windows `curl.exe -fsSL https://dl.google.com/android/cli/latest/windows_x86_64/install.cmd -o \"%TEMP%\\i.cmd\" && \"%TEMP%\\i.cmd\"`.",
"uninstall_notes": "Remove the Android CLI binary from your PATH or rerun the official installer cleanup flow for your platform.",
"update_notes": "Run `android update` to fetch the latest Android CLI capabilities, or reinstall from the official archive page if needed.",
"skill_md": "https://developer.android.com/tools/agents/android-skills",
"entry_point": "android",
"detect_cmd": "android",
"contributors": [
{
"name": "Android Developers",
"url": "https://developer.android.com"
}
]
},
{
"name": "generate-veo-video",
"display_name": "Generate Veo Video",
@@ -187,6 +220,7 @@
"install_cmd": "uv tool install git+https://github.com/charles-forsyth/generate-veo-video.git",
"uninstall_cmd": "uv tool uninstall generate-veo-video",
"update_cmd": "uv tool upgrade generate-veo-video",
"skill_md": "https://github.com/charles-forsyth/generate-veo-video",
"entry_point": "generate-veo",
"contributors": [
{
@@ -195,6 +229,51 @@
}
]
},
{
"name": "suno",
"display_name": "Suno CLI",
"version": "latest",
"description": "CLI for generating music with Suno AI from lyrics and style prompts, with batch generation, status polling, downloads, and automatic MP3 tagging",
"category": "music",
"requires": "Python 3 and a Suno API key from sunoapi.org",
"homepage": "https://github.com/slauger/suno-cli",
"source_url": "https://github.com/slauger/suno-cli",
"package_manager": "pip",
"install_strategy": "command",
"install_cmd": "python3 -m pip install git+https://github.com/slauger/suno-cli.git",
"uninstall_cmd": "python3 -m pip uninstall -y suno-cli",
"update_cmd": "python3 -m pip install --upgrade --force-reinstall git+https://github.com/slauger/suno-cli.git",
"skill_md": "https://github.com/slauger/suno-cli/blob/main/docs/USAGE.md",
"entry_point": "suno",
"contributors": [
{
"name": "slauger",
"url": "https://github.com/slauger"
}
]
},
{
"name": "elevenlabs",
"display_name": "ElevenLabs CLI",
"version": "latest",
"description": "Official ElevenLabs CLI for managing voice agents as code with local configs, templates, auth, push/pull sync, tests, widgets, and branch-aware workflows",
"category": "audio",
"requires": "Node.js and npm, ELEVENLABS_API_KEY or interactive login",
"homepage": "https://github.com/elevenlabs/cli",
"source_url": "https://github.com/elevenlabs/cli",
"package_manager": "npm",
"npm_package": "@elevenlabs/cli",
"install_cmd": "npm install -g @elevenlabs/cli",
"npx_cmd": "npx @elevenlabs/cli@latest",
"skill_md": "https://github.com/elevenlabs/cli",
"entry_point": "elevenlabs",
"contributors": [
{
"name": "ElevenLabs",
"url": "https://github.com/elevenlabs"
}
]
},
{
"name": "jimeng",
"display_name": "Jimeng / Dreamina CLI",
@@ -208,6 +287,7 @@
"package_manager": "script",
"install_strategy": "command",
"install_cmd": "curl -s https://jimeng.jianying.com/cli | bash",
"skill_md": "https://bytedance.larkoffice.com/wiki/FVTwwm0bGiishxkKOoScdHR2nsg",
"entry_point": "dreamina",
"contributors": [
{
@@ -230,6 +310,7 @@
"install_notes": "Bundled within the Obsidian 1.12+ installer. Enable Command line interface in Obsidian settings and follow the registration prompt so `obsidian` is added to PATH.",
"uninstall_notes": "Disable Command line interface from Obsidian settings or remove/update the Obsidian installer to stop using the bundled CLI.",
"update_notes": "Update the Obsidian installer to a newer 1.12+ release to update the bundled CLI.",
"skill_md": "https://obsidian.md/help/cli",
"entry_point": "obsidian",
"detect_cmd": "obsidian",
"contributors": [
@@ -238,6 +319,26 @@
"url": "https://obsidian.md"
}
]
},
{
"name": "py4csr",
"display_name": "TraceCSR / Py4CSR CLI",
"version": "latest",
"description": "GxP-compliant agent harness for CDISC Clinical Study Report (CSR) and Tables/Figures/Listings (TFL) generation",
"category": "data-science",
"requires": "Python >= 3.10",
"homepage": "https://github.com/yanmingyu92/py4csr",
"source_url": "https://github.com/yanmingyu92/py4csr",
"skill_md": "https://github.com/yanmingyu92/py4csr",
"package_manager": "pip",
"install_cmd": "pip install py4csr",
"entry_point": "tracecsr",
"contributors": [
{
"name": "yanmingyu92",
"url": "https://github.com/yanmingyu92"
}
]
}
]
}
+59 -40
View File
@@ -15,7 +15,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=wiremock/agent-harness",
"entry_point": "cli-anything-wiremock",
"skill_md": "wiremock/agent-harness/cli_anything/wiremock/skills/SKILL.md",
"skill_md": "skills/cli-anything-wiremock/SKILL.md",
"category": "testing",
"contributors": [
{
@@ -34,7 +34,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=anygen/agent-harness",
"entry_point": "cli-anything-anygen",
"skill_md": "anygen/agent-harness/cli_anything/anygen/skills/SKILL.md",
"skill_md": "skills/cli-anything-anygen/SKILL.md",
"category": "generation",
"contributors": [
{
@@ -72,7 +72,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=audacity/agent-harness",
"entry_point": "cli-anything-audacity",
"skill_md": "audacity/agent-harness/cli_anything/audacity/skills/SKILL.md",
"skill_md": "skills/cli-anything-audacity/SKILL.md",
"category": "audio",
"contributors": [
{
@@ -91,7 +91,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=blender/agent-harness",
"entry_point": "cli-anything-blender",
"skill_md": "blender/agent-harness/cli_anything/blender/skills/SKILL.md",
"skill_md": "skills/cli-anything-blender/SKILL.md",
"category": "3d",
"contributors": [
{
@@ -110,7 +110,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=browser/agent-harness",
"entry_point": "cli-anything-browser",
"skill_md": "browser/agent-harness/cli_anything/browser/skills/SKILL.md",
"skill_md": "skills/cli-anything-browser/SKILL.md",
"category": "web",
"contributors": [
{
@@ -148,7 +148,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=drawio/agent-harness",
"entry_point": "cli-anything-drawio",
"skill_md": "drawio/agent-harness/cli_anything/drawio/skills/SKILL.md",
"skill_md": "skills/cli-anything-drawio/SKILL.md",
"category": "diagrams",
"contributors": [
{
@@ -167,7 +167,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=eth2-quickstart/agent-harness",
"entry_point": "cli-anything-eth2-quickstart",
"skill_md": "eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/skills/SKILL.md",
"skill_md": "skills/cli-anything-eth2-quickstart/SKILL.md",
"category": "devops",
"contributors": [
{
@@ -186,7 +186,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=gimp/agent-harness",
"entry_point": "cli-anything-gimp",
"skill_md": "gimp/agent-harness/cli_anything/gimp/skills/SKILL.md",
"skill_md": "skills/cli-anything-gimp/SKILL.md",
"category": "image",
"contributors": [
{
@@ -205,7 +205,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=inkscape/agent-harness",
"entry_point": "cli-anything-inkscape",
"skill_md": "inkscape/agent-harness/cli_anything/inkscape/skills/SKILL.md",
"skill_md": "skills/cli-anything-inkscape/SKILL.md",
"category": "image",
"contributors": [
{
@@ -224,7 +224,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=kdenlive/agent-harness",
"entry_point": "cli-anything-kdenlive",
"skill_md": "kdenlive/agent-harness/cli_anything/kdenlive/skills/SKILL.md",
"skill_md": "skills/cli-anything-kdenlive/SKILL.md",
"category": "video",
"contributors": [
{
@@ -243,7 +243,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=krita/agent-harness",
"entry_point": "cli-anything-krita",
"skill_md": "krita/agent-harness/cli_anything/krita/skills/SKILL.md",
"skill_md": "skills/cli-anything-krita/SKILL.md",
"category": "image",
"contributors": [
{
@@ -262,7 +262,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=libreoffice/agent-harness",
"entry_point": "cli-anything-libreoffice",
"skill_md": "libreoffice/agent-harness/cli_anything/libreoffice/skills/SKILL.md",
"skill_md": "skills/cli-anything-libreoffice/SKILL.md",
"category": "office",
"contributors": [
{
@@ -304,7 +304,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=mubu/agent-harness",
"entry_point": "cli-anything-mubu",
"skill_md": "mubu/agent-harness/cli_anything/mubu/skills/SKILL.md",
"skill_md": "skills/cli-anything-mubu/SKILL.md",
"category": "office",
"contributors": [
{
@@ -342,7 +342,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=notebooklm/agent-harness",
"entry_point": "cli-anything-notebooklm",
"skill_md": "notebooklm/agent-harness/cli_anything/notebooklm/skills/SKILL.md",
"skill_md": "skills/cli-anything-notebooklm/SKILL.md",
"category": "ai",
"contributors": [
{
@@ -361,7 +361,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=ollama/agent-harness",
"entry_point": "cli-anything-ollama",
"skill_md": "ollama/agent-harness/cli_anything/ollama/skills/SKILL.md",
"skill_md": "skills/cli-anything-ollama/SKILL.md",
"category": "ai",
"contributors": [
{
@@ -380,7 +380,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=obs-studio/agent-harness",
"entry_point": "cli-anything-obs-studio",
"skill_md": "obs-studio/agent-harness/cli_anything/obs_studio/skills/SKILL.md",
"skill_md": "skills/cli-anything-obs-studio/SKILL.md",
"category": "streaming",
"contributors": [
{
@@ -399,7 +399,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=shotcut/agent-harness",
"entry_point": "cli-anything-shotcut",
"skill_md": "shotcut/agent-harness/cli_anything/shotcut/skills/SKILL.md",
"skill_md": "skills/cli-anything-shotcut/SKILL.md",
"category": "video",
"contributors": [
{
@@ -418,7 +418,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=openscreen/agent-harness",
"entry_point": "cli-anything-openscreen",
"skill_md": "openscreen/agent-harness/cli_anything/openscreen/skills/SKILL.md",
"skill_md": "skills/cli-anything-openscreen/SKILL.md",
"category": "video",
"contributors": [
{
@@ -437,7 +437,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=zoom/agent-harness",
"entry_point": "cli-anything-zoom",
"skill_md": "zoom/agent-harness/cli_anything/zoom/skills/SKILL.md",
"skill_md": "skills/cli-anything-zoom/SKILL.md",
"category": "communication",
"contributors": [
{
@@ -456,7 +456,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=novita/agent-harness",
"entry_point": "cli-anything-novita",
"skill_md": "novita/agent-harness/cli_anything/novita/skills/SKILL.md",
"skill_md": "skills/cli-anything-novita/SKILL.md",
"category": "ai",
"contributors": [
{
@@ -475,7 +475,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=seaclip/agent-harness",
"entry_point": "cli-anything-seaclip",
"skill_md": "seaclip/agent-harness/cli_anything/seaclip/skills/SKILL.md",
"skill_md": "skills/cli-anything-seaclip/SKILL.md",
"category": "project-management",
"contributors": [
{
@@ -494,7 +494,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=pm2/agent-harness",
"entry_point": "cli-anything-pm2",
"skill_md": "pm2/agent-harness/cli_anything/pm2/skills/SKILL.md",
"skill_md": "skills/cli-anything-pm2/SKILL.md",
"category": "devops",
"contributors": [
{
@@ -527,7 +527,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=chromadb/agent-harness",
"entry_point": "cli-anything-chromadb",
"skill_md": "chromadb/agent-harness/cli_anything/chromadb/skills/SKILL.md",
"skill_md": "skills/cli-anything-chromadb/SKILL.md",
"category": "database",
"contributors": [
{
@@ -546,7 +546,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=musescore/agent-harness",
"entry_point": "cli-anything-musescore",
"skill_md": "musescore/agent-harness/cli_anything/musescore/skills/SKILL.md",
"skill_md": "skills/cli-anything-musescore/SKILL.md",
"category": "music",
"contributors": [
{
@@ -584,7 +584,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=freecad/agent-harness",
"entry_point": "cli-anything-freecad",
"skill_md": "freecad/agent-harness/cli_anything/freecad/skills/SKILL.md",
"skill_md": "skills/cli-anything-freecad/SKILL.md",
"category": "3d",
"contributors": [
{
@@ -603,7 +603,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=iterm2/agent-harness",
"entry_point": "cli-anything-iterm2",
"skill_md": "iterm2/agent-harness/cli_anything/iterm2_ctl/skills/SKILL.md",
"skill_md": "skills/cli-anything-iterm2/SKILL.md",
"category": "devops",
"contributors": [
{
@@ -622,7 +622,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=slay_the_spire_ii/agent-harness",
"entry_point": "cli-anything-sts2",
"skill_md": "slay_the_spire_ii/agent-harness/cli_anything/slay_the_spire_ii/skills/SKILL.md",
"skill_md": "skills/cli-anything-slay-the-spire-ii/SKILL.md",
"category": "game",
"contributors": [
{
@@ -641,7 +641,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=rms/agent-harness",
"entry_point": "cli-anything-rms",
"skill_md": "rms/agent-harness/cli_anything/rms/skills/SKILL.md",
"skill_md": "skills/cli-anything-rms/SKILL.md",
"category": "network",
"contributors": [
{
@@ -660,7 +660,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=renderdoc/agent-harness",
"entry_point": "cli-anything-renderdoc",
"skill_md": "renderdoc/agent-harness/cli_anything/renderdoc/skills/SKILL.md",
"skill_md": "skills/cli-anything-renderdoc/SKILL.md",
"category": "graphics",
"contributors": [
{
@@ -698,7 +698,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=videocaptioner/agent-harness",
"entry_point": "cli-anything-videocaptioner",
"skill_md": "videocaptioner/agent-harness/cli_anything/videocaptioner/skills/SKILL.md",
"skill_md": "skills/cli-anything-videocaptioner/SKILL.md",
"category": "video",
"contributors": [
{
@@ -717,7 +717,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=intelwatch/agent-harness",
"entry_point": "cli-anything-intelwatch",
"skill_md": "intelwatch/agent-harness/cli_anything/intelwatch/skills/SKILL.md",
"skill_md": "skills/cli-anything-intelwatch/SKILL.md",
"category": "osint",
"contributors": [
{
@@ -755,7 +755,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=cloudcompare/agent-harness",
"entry_point": "cli-anything-cloudcompare",
"skill_md": "cloudcompare/agent-harness/cli_anything/cloudcompare/skills/SKILL.md",
"skill_md": "skills/cli-anything-cloudcompare/SKILL.md",
"category": "graphics",
"contributors": [
{
@@ -774,7 +774,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=exa/agent-harness",
"entry_point": "cli-anything-exa",
"skill_md": "exa/agent-harness/cli_anything/exa/skills/SKILL.md",
"skill_md": "skills/cli-anything-exa/SKILL.md",
"category": "search",
"contributors": [
{
@@ -793,7 +793,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=godot/agent-harness",
"entry_point": "cli-anything-godot",
"skill_md": "godot/agent-harness/cli_anything/godot/skills/SKILL.md",
"skill_md": "skills/cli-anything-godot/SKILL.md",
"category": "gamedev",
"contributors": [
{
@@ -812,7 +812,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=dify-workflow/agent-harness",
"entry_point": "cli-anything-dify-workflow",
"skill_md": "dify-workflow/agent-harness/cli_anything/dify_workflow/skills/SKILL.md",
"skill_md": "skills/cli-anything-dify-workflow/SKILL.md",
"category": "ai",
"contributors": [
{
@@ -831,7 +831,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=n8n/agent-harness",
"entry_point": "cli-anything-n8n",
"skill_md": "n8n/agent-harness/cli_anything/n8n/skills/SKILL.md",
"skill_md": "skills/cli-anything-n8n/SKILL.md",
"category": "automation",
"contributors": [
{
@@ -850,7 +850,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=cloudanalyzer/agent-harness",
"entry_point": "cli-anything-cloudanalyzer",
"skill_md": "cloudanalyzer/agent-harness/cli_anything/cloudanalyzer/skills/SKILL.md",
"skill_md": "skills/cli-anything-cloudanalyzer/SKILL.md",
"category": "graphics",
"contributors": [
{
@@ -869,7 +869,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=obsidian/agent-harness",
"entry_point": "cli-anything-obsidian",
"skill_md": "obsidian/agent-harness/cli_anything/obsidian/skills/SKILL.md",
"skill_md": "skills/cli-anything-obsidian/SKILL.md",
"category": "knowledge",
"contributors": [
{
@@ -888,7 +888,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=unimol_tools/agent-harness",
"entry_point": "cli-anything-unimol-tools",
"skill_md": "unimol_tools/agent-harness/cli_anything/unimol_tools/SKILL.md",
"skill_md": "skills/cli-anything-unimol-tools/SKILL.md",
"category": "science",
"contributors": [
{
@@ -907,7 +907,7 @@
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=safari/agent-harness",
"entry_point": "cli-anything-safari",
"skill_md": "safari/agent-harness/cli_anything/safari/skills/SKILL.md",
"skill_md": "skills/cli-anything-safari/SKILL.md",
"category": "web",
"contributors": [
{
@@ -915,6 +915,25 @@
"url": "https://github.com/achiya-automation"
}
]
},
{
"name": "openclaw-macro",
"display_name": "OpenClaw Macro System",
"version": "1.0.0",
"description": "Layered CLI that converts GUI workflows into parameterized, agent-callable macros — with backend routing across native APIs, file transforms, accessibility controls, and compiled GUI replay",
"requires": null,
"homepage": "https://github.com/HKUDS/CLI-Anything",
"source_url": null,
"install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=openclaw-skill/agent-harness",
"entry_point": "cli-anything-openclaw",
"skill_md": "openclaw-skill/agent-harness/cli_anything/openclaw/skills/SKILL.md",
"category": "automation",
"contributors": [
{
"name": "haorui-harry",
"url": "https://github.com/haorui-harry"
}
]
}
]
}
@@ -7,7 +7,7 @@ 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
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -57,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -89,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -106,27 +120,44 @@ class ReplSkin:
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.
Auto-detected from the repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Auto-detect skill path from package layout:
# cli_anything/<software>/utils/repl_skin.py (this file)
# cli_anything/<software>/skills/SKILL.md (target)
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
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)
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -156,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -164,6 +197,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -178,19 +229,14 @@ class ReplSkin:
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}"
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
if skill_line:
print(_box_line(skill_line))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -7,7 +7,7 @@ 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
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -47,9 +48,6 @@ _ACCENT_COLORS = {
"obs_studio": "\033[38;5;55m", # purple
"kdenlive": "\033[38;5;69m", # slate blue
"shotcut": "\033[38;5;35m", # teal green
"anygen": "\033[38;5;141m", # soft violet
"novita": "\033[38;5;81m", # vivid blue (for Novita AI)
"rms": "\033[38;5;27m", # Teltonika blue
}
_DEFAULT_ACCENT = "\033[38;5;75m" # default sky blue
@@ -60,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -92,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -109,27 +120,44 @@ class ReplSkin:
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.
Auto-detected from the repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Auto-detect skill path from package layout:
# cli_anything/<software>/utils/repl_skin.py (this file)
# cli_anything/<software>/skills/SKILL.md (target)
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
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)
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -159,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -167,6 +197,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -181,19 +229,14 @@ class ReplSkin:
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}"
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
if skill_line:
print(_box_line(skill_line))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -7,7 +7,7 @@ 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
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -47,7 +48,6 @@ _ACCENT_COLORS = {
"obs_studio": "\033[38;5;55m", # purple
"kdenlive": "\033[38;5;69m", # slate blue
"shotcut": "\033[38;5;35m", # teal green
"safari": "\033[38;5;33m", # Safari blue
}
_DEFAULT_ACCENT = "\033[38;5;75m" # default sky blue
@@ -58,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -90,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -107,27 +120,44 @@ class ReplSkin:
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.
Auto-detected from the repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Auto-detect skill path from package layout:
# cli_anything/<software>/utils/repl_skin.py (this file)
# cli_anything/<software>/skills/SKILL.md (target)
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
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)
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -157,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -165,6 +197,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -179,19 +229,14 @@ class ReplSkin:
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}"
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
if skill_line:
print(_box_line(skill_line))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -6,20 +6,21 @@ Copy this file into your CLI package at:
Usage:
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("ollama", version="1.0.0")
skin.print_banner()
prompt_text = skin.prompt(project_name="llama3.2", modified=False)
skin.success("Model pulled")
skin.error("Connection failed")
skin.warning("No models loaded")
skin.info("Generating...")
skin.status("Model", "llama3.2:latest")
skin = ReplSkin("shotcut", version="1.0.0")
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
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
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -47,7 +48,6 @@ _ACCENT_COLORS = {
"obs_studio": "\033[38;5;55m", # purple
"kdenlive": "\033[38;5;69m", # slate blue
"shotcut": "\033[38;5;35m", # teal green
"ollama": "\033[38;5;255m", # white (Ollama branding)
}
_DEFAULT_ACCENT = "\033[38;5;75m" # default sky blue
@@ -58,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -90,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -98,23 +111,53 @@ class ReplSkin:
"""
def __init__(self, software: str, version: str = "1.0.0",
history_file: str | None = None):
history_file: str | None = None, skill_path: str | None = None):
"""Initialize the REPL skin.
Args:
software: Software name (e.g., "gimp", "shotcut", "ollama").
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 repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
if skill_path is None:
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -144,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -152,10 +197,28 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
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 · Ollama
# Title: ◆ cli-anything · Shotcut
icon = self._c(_CYAN + _BOLD, "")
brand = self._c(_CYAN + _BOLD, "cli-anything")
dot = self._c(_DARK_GRAY, "·")
@@ -166,9 +229,14 @@ class ReplSkin:
tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}"
empty = ""
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
@@ -496,5 +564,4 @@ _ANSI_256_TO_HEX = {
"\033[38;5;80m": "#5fd7d7", # brand cyan
"\033[38;5;208m": "#ff8700", # blender deep orange
"\033[38;5;214m": "#ffaf00", # gimp warm orange
"\033[38;5;255m": "#eeeeee", # ollama white
}
+1
View File
@@ -0,0 +1 @@
.worktrees/
@@ -6,6 +6,7 @@ import shutil
from typing import Optional
from ..utils import mlt_xml
from ..utils.time import timecode_to_frames, frames_to_timecode
from .session import Session
@@ -411,6 +412,57 @@ def render(session: Session, output_path: str,
width, height)
def _set_tractor_out(session: Session) -> None:
"""Set tractor out= to actual timeline duration before passing to melt.
The tractor is created with out="00:00:00.000" and never updated as clips
are added. Without this fix, melt falls back to the longest track in the
multitrack the 4-hour black background and renders a 4-hour file.
"""
profile = session.get_profile()
fps_num = int(profile.get("frame_rate_num", 30000))
fps_den = int(profile.get("frame_rate_den", 1001))
tractor = mlt_xml.get_main_tractor(session.root)
if tractor is None:
return
tracks = mlt_xml.get_tractor_tracks(tractor)
max_frames = 0
for te in tracks:
prod_id = te.get("producer", "")
if prod_id == "background":
continue
playlist = mlt_xml.find_element_by_id(session.root, prod_id)
if playlist is None:
continue
total = 0
for child in playlist:
if child.tag == "entry":
in_f = timecode_to_frames(child.get("in", "0"), fps_num, fps_den)
out_f = timecode_to_frames(child.get("out", "0"), fps_num, fps_den)
total += max(0, out_f - in_f + 1)
elif child.tag == "blank":
try:
total += timecode_to_frames(child.get("length", "0"), fps_num, fps_den)
except Exception:
pass
max_frames = max(max_frames, total)
if max_frames > 0:
out_tc = frames_to_timecode(max_frames - 1, fps_num, fps_den)
tractor.set("out", out_tc)
# Cap the background track to the same duration so melt doesn't
# extend the render to the 4-hour background default.
bg_playlist = mlt_xml.find_element_by_id(session.root, "background")
if bg_playlist is not None:
for entry in bg_playlist.findall("entry"):
entry.set("out", out_tc)
black_producer = session.root.find(".//producer[@id='black']")
if black_producer is not None:
black_producer.set("out", out_tc)
def _render_with_melt(session: Session, output_path: str,
preset: dict, melt_path: str,
width: Optional[int], height: Optional[int],
@@ -418,6 +470,10 @@ def _render_with_melt(session: Session, output_path: str,
"""Render using melt command."""
import tempfile
# Fix tractor out before rendering — without this melt renders the full
# 4-hour background track instead of the actual content duration.
_set_tractor_out(session)
# Save project to temp file
with tempfile.NamedTemporaryFile(suffix=".mlt", delete=False, mode="w") as f:
temp_mlt = f.name
@@ -25,8 +25,10 @@ from cli_anything.shotcut.utils.mlt_xml import (
create_blank_project, mlt_to_string, parse_mlt, write_mlt,
get_property, set_property, get_main_tractor, get_tractor_tracks,
get_all_producers, get_playlist_entries, find_element_by_id,
add_filter_to_element,
add_filter_to_element, add_track_to_tractor, add_entry_to_playlist,
add_blank_to_playlist,
)
from cli_anything.shotcut.utils.time import frames_to_timecode as _ftc
# ============================================================================
@@ -689,6 +691,70 @@ class TestExport:
finally:
os.unlink(tmpfile)
def test_set_tractor_out_single_clip(self):
"""_set_tractor_out sets tractor out to match a single clip duration."""
s = Session()
proj_mod.new_project(s, "hd1080p30")
tractor = get_main_tractor(s.root)
playlist_id, _ = add_track_to_tractor(s.root, tractor, "video", "V1")
playlist = find_element_by_id(s.root, playlist_id)
# Add a 6-second clip (frames 0179 at 29.97fps ≈ 180 frames)
prod = s.root.makeelement("producer", {"id": "clip1"})
s.root.insert(0, prod)
add_entry_to_playlist(playlist, "clip1", "00:00:00.000", "00:00:05.999")
# Before fix: tractor out is still the initial value
assert tractor.get("out") == "00:00:00.000"
export_mod._set_tractor_out(s)
# After fix: tractor out should reflect the clip duration
assert tractor.get("out") != "00:00:00.000"
assert tractor.get("out") != "04:00:00.000"
# Background entry should be capped too
bg = find_element_by_id(s.root, "background")
bg_entry = bg.find("entry")
assert bg_entry.get("out") == tractor.get("out")
# Black producer should be capped
black = s.root.find(".//producer[@id='black']")
assert black.get("out") == tractor.get("out")
def test_set_tractor_out_multi_segment(self):
"""_set_tractor_out sums entry spans and blanks across segments."""
s = Session()
proj_mod.new_project(s, "hd1080p30")
tractor = get_main_tractor(s.root)
pid, _ = add_track_to_tractor(s.root, tractor, "video", "V1")
playlist = find_element_by_id(s.root, pid)
# Two 3-second clips with a 1-second blank between
prod1 = s.root.makeelement("producer", {"id": "seg1"})
prod2 = s.root.makeelement("producer", {"id": "seg2"})
s.root.insert(0, prod1)
s.root.insert(0, prod2)
add_entry_to_playlist(playlist, "seg1", "00:00:00.000", "00:00:02.999")
add_blank_to_playlist(playlist, "00:00:01.000")
add_entry_to_playlist(playlist, "seg2", "00:00:00.000", "00:00:02.999")
export_mod._set_tractor_out(s)
# Tractor out should cover all segments + blank (~7 seconds)
out_tc = tractor.get("out")
assert out_tc != "00:00:00.000"
assert out_tc != "04:00:00.000"
def test_set_tractor_out_empty_timeline(self):
"""_set_tractor_out is a no-op on a blank project with no clips."""
s = Session()
proj_mod.new_project(s, "hd1080p30")
tractor = get_main_tractor(s.root)
export_mod._set_tractor_out(s)
# No clips → tractor out should stay unchanged
assert tractor.get("out") == "00:00:00.000"
# ============================================================================
# Integration: full workflow
@@ -7,7 +7,7 @@ Usage:
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("shotcut", version="1.0.0")
skin.print_banner()
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
@@ -20,6 +20,7 @@ Usage:
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
@@ -57,6 +58,8 @@ _RED = "\033[38;5;196m"
_BLUE = "\033[38;5;75m"
_MAGENTA = "\033[38;5;176m"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
@@ -89,6 +92,17 @@ def _visible_len(text: str) -> int:
return len(_strip_ansi(text))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
@@ -97,7 +111,7 @@ class ReplSkin:
"""
def __init__(self, software: str, version: str = "1.0.0",
history_file: str | None = None):
history_file: str | None = None, skill_path: str | None = None):
"""Initialize the REPL skin.
Args:
@@ -105,15 +119,45 @@ class ReplSkin:
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 repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
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
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
if skill_path is None:
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
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")
@@ -143,7 +187,9 @@ class ReplSkin:
def print_banner(self):
"""Print the startup banner with branding."""
inner = 54
import textwrap
inner = 72
def _box_line(content: str) -> str:
"""Wrap content in box drawing, padding to inner width."""
@@ -151,6 +197,24 @@ class ReplSkin:
vl = self._c(_DARK_GRAY, _V_LINE)
return f"{vl}{content}{' ' * max(0, pad)}{vl}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
@@ -165,9 +229,14 @@ class ReplSkin:
tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}"
empty = ""
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
+31
View File
@@ -0,0 +1,31 @@
# CLI-Anything Skills
This directory is the canonical `npx skills` surface for in-repo CLI-Anything
harnesses.
Layout:
```text
skills/
cli-anything-audacity/SKILL.md
cli-anything-blender/SKILL.md
...
```
Typical usage:
```bash
npx skills add HKUDS/CLI-Anything --list
npx skills add HKUDS/CLI-Anything --skill cli-anything-audacity -g -y
```
The `SKILL.md` files here are the canonical repo-root copies. Installed harness
packages still ship compatibility copies inside `cli_anything/<software>/skills/`
for local runtime discovery.
CI rule:
- If a harness keeps a deep packaged `SKILL.md`, it must also have a matching
repo-root `skills/<skill-id>/SKILL.md`.
- A future harness that only defines its canonical skill directly in `skills/`
is also valid.
+255
View File
@@ -0,0 +1,255 @@
---
name: "cli-anything-adguardhome"
description: >-
Command-line interface for AdGuard Home - Network-wide ad blocking and DNS management via AdGuard Home REST API. Designed for AI agents and power users who need to manage filtering, DNS rewrites, clients, DHCP, and query logs without a GUI.
---
# cli-anything-adguardhome
Network-wide ad blocking and DNS management via the AdGuard Home REST API. Designed for AI agents and power users who need to manage filtering, DNS rewrites, clients, DHCP, and query logs without a GUI.
## Installation
This CLI is installed as part of the cli-anything-adguardhome package:
```bash
pip install cli-anything-adguardhome
```
**Prerequisites:**
- Python 3.10+
- AdGuard Home must be installed and running
- Install AdGuard Home: `curl -s -S -L https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -v`
## Usage
### Basic Commands
```bash
# Show help
cli-anything-adguardhome --help
# Start interactive REPL mode
cli-anything-adguardhome
# Check server status
cli-anything-adguardhome server status
# Run with JSON output (for agent consumption)
cli-anything-adguardhome --json server status
```
### REPL Mode
When invoked without a subcommand, the CLI enters an interactive REPL session:
```bash
cli-anything-adguardhome
# Enter commands interactively with tab-completion and history
```
## Command Groups
### Config
Connection and configuration management.
| Command | Description |
|---------|-------------|
| `show` | Show current connection configuration |
| `save` | Save connection settings to a config file |
| `test` | Test the connection to AdGuard Home |
### Server
Server status and control commands.
| Command | Description |
|---------|-------------|
| `status` | Show server protection status |
| `version` | Show AdGuard Home version |
| `restart` | Restart the AdGuard Home server |
### Filter
DNS filter list management.
| Command | Description |
|---------|-------------|
| `list` | List all configured filter lists |
| `status` | Show filtering status |
| `toggle` | Enable or disable filtering globally |
| `add` | Add a new filter list by URL |
| `remove` | Remove a filter list |
| `enable` | Enable a specific filter list |
| `disable` | Disable a specific filter list |
| `refresh` | Force-refresh all filter lists |
### Blocking
Parental control, safe browsing, and safe search settings.
| Command | Description |
|---------|-------------|
| `parental status` | Show parental control status |
| `parental enable` | Enable parental control |
| `parental disable` | Disable parental control |
| `safebrowsing status` | Show safe browsing status |
| `safebrowsing enable` | Enable safe browsing |
| `safebrowsing disable` | Disable safe browsing |
| `safesearch status` | Show safe search status |
| `safesearch enable` | Enable safe search |
| `safesearch disable` | Disable safe search |
### Blocked-Services
Manage blocked internet services.
| Command | Description |
|---------|-------------|
| `list` | List currently blocked services |
| `set` | Set the list of blocked services |
### Clients
Client device management.
| Command | Description |
|---------|-------------|
| `list` | List all configured clients |
| `add` | Add a new client by name and IP |
| `remove` | Remove a client |
| `show` | Show details for a specific client |
### Stats
Query statistics.
| Command | Description |
|---------|-------------|
| `show` | Show DNS query statistics |
| `reset` | Reset all statistics |
| `config` | View or update statistics retention interval |
### Log
DNS query log management.
| Command | Description |
|---------|-------------|
| `show` | Show recent DNS query log entries |
| `config` | View or update query log settings |
| `clear` | Clear the query log |
### Rewrite
DNS rewrite rules.
| Command | Description |
|---------|-------------|
| `list` | List all DNS rewrite rules |
| `add` | Add a DNS rewrite rule |
| `remove` | Remove a DNS rewrite rule |
### DHCP
DHCP server management.
| Command | Description |
|---------|-------------|
| `status` | Show DHCP server status |
| `leases` | List active DHCP leases |
| `add-static` | Add a static DHCP lease |
| `remove-static` | Remove a static DHCP lease |
### TLS
TLS/HTTPS configuration.
| Command | Description |
|---------|-------------|
| `status` | Show TLS configuration status |
## Examples
### Check Server Status
```bash
cli-anything-adguardhome server status
cli-anything-adguardhome server version
```
### Manage Filter Lists
```bash
# List current filters
cli-anything-adguardhome filter list
# Add a new blocklist
cli-anything-adguardhome filter add --url https://somehost.com/list.txt --name "My List"
# Refresh all filters
cli-anything-adguardhome filter refresh
```
### DNS Rewrites
```bash
# Add a local DNS entry
cli-anything-adguardhome rewrite add --domain "myserver.local" --answer "192.168.1.50"
# List all rewrites
cli-anything-adguardhome rewrite list
```
### Client Management
```bash
cli-anything-adguardhome clients add --name "My PC" --ip 192.168.1.100
cli-anything-adguardhome clients list
```
### Query Statistics
```bash
# Show stats (human-readable)
cli-anything-adguardhome stats show
# Show stats (JSON for agents)
cli-anything-adguardhome --json stats show
```
## 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-adguardhome filter list
# JSON output for agents
cli-anything-adguardhome --json filter list
```
## 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. **Test connection first** with `config test` before other commands
## 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
1.0.0
+173
View File
@@ -0,0 +1,173 @@
---
name: "cli-anything-anygen"
description: >-
Command-line interface for Anygen - A stateful command-line interface for AnyGen OpenAPI — generate professional slides, documents, webs...
---
# cli-anything-anygen
A stateful command-line interface for AnyGen OpenAPI — generate professional slides, documents, websites, diagrams, and more from natural language prompts. Designed for AI agents and power users.
## Installation
This CLI is installed as part of the cli-anything-anygen package:
```bash
pip install cli-anything-anygen
```
**Prerequisites:**
- Python 3.10+
- anygen must be installed on your system
## Usage
### Basic Commands
```bash
# Show help
cli-anything-anygen --help
# Start interactive REPL mode
cli-anything-anygen
# Create a new project
cli-anything-anygen project new -o project.json
# Run with JSON output (for agent consumption)
cli-anything-anygen --json project info -p project.json
```
### REPL Mode
When invoked without a subcommand, the CLI enters an interactive REPL session:
```bash
cli-anything-anygen
# Enter commands interactively with tab-completion and history
```
## Command Groups
### Task
Task management — create, poll, download, and run tasks.
| Command | Description |
|---------|-------------|
| `create` | Create a generation task |
| `status` | Query task status (non-blocking) |
| `poll` | Poll task until completion (blocking) |
| `download` | Download the generated file for a completed task |
| `thumbnail` | Download thumbnail image for a completed task |
| `run` | Full workflow: create, poll, download |
| `list` | List locally cached task records |
| `prepare` | Multi-turn requirement analysis before creating a task |
### File
File operations — upload reference files.
| Command | Description |
|---------|-------------|
| `upload` | Upload a reference file to get a file_token |
### Config
Configuration management — API key and settings.
| Command | Description |
|---------|-------------|
| `set` | Set a configuration value |
| `get` | Get a configuration value (or show all) |
| `delete` | Delete a configuration value |
| `path` | Show the config file path |
### Session
Session management — history, undo, redo.
| Command | Description |
|---------|-------------|
| `status` | Show session status |
| `history` | Show command history |
| `undo` | Undo last command |
| `redo` | Redo last undone command |
## Examples
### Create a New Project
Create a new anygen project file.
```bash
cli-anything-anygen project new -o myproject.json
# Or with JSON output for programmatic use
cli-anything-anygen --json project new -o myproject.json
```
### Interactive REPL Session
Start an interactive session with undo/redo support.
```bash
cli-anything-anygen
# Enter commands interactively
# Use 'help' to see available commands
# Use 'undo' and 'redo' for history navigation
```
## 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-anygen project info -p project.json
# JSON output for agents
cli-anything-anygen --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
1.0.0
+244
View File
@@ -0,0 +1,244 @@
---
name: "cli-anything-audacity"
description: >-
Command-line interface for Audacity - A stateful command-line interface for audio editing, following the same patterns as the GIMP and Ble...
---
# cli-anything-audacity
A stateful command-line interface for audio editing, following the same patterns as the GIMP and Blender CLIs in this repo.
## Installation
This CLI is installed as part of the cli-anything-audacity package:
```bash
pip install cli-anything-audacity
```
**Prerequisites:**
- Python 3.10+
- audacity must be installed on your system
## Usage
### Basic Commands
```bash
# Show help
cli-anything-audacity --help
# Start interactive REPL mode
cli-anything-audacity
# Create a new project
cli-anything-audacity project new -o project.json
# Run with JSON output (for agent consumption)
cli-anything-audacity --json project info -p project.json
```
### REPL Mode
When invoked without a subcommand, the CLI enters an interactive REPL session:
```bash
cli-anything-audacity
# Enter commands interactively with tab-completion and history
```
## Command Groups
### Project
Project management commands.
| Command | Description |
|---------|-------------|
| `new` | Create a new project |
| `open` | Open an existing project |
| `save` | Save the current project |
| `info` | Show project information |
| `settings` | View or update project settings |
| `json` | Print raw project JSON |
### Track
Track management commands.
| Command | Description |
|---------|-------------|
| `add` | Add a new track |
| `remove` | Remove a track by index |
| `list` | List all tracks |
| `set` | Set a track property (name, mute, solo, volume, pan) |
### Clip
Clip management commands.
| Command | Description |
|---------|-------------|
| `import` | Probe/import an audio file (show metadata) |
| `add` | Add an audio clip to a track |
| `remove` | Remove a clip from a track |
| `trim` | Trim a clip's start and/or end |
| `split` | Split a clip at a given time position |
| `move` | Move a clip to a new start time |
| `list` | List clips on a track |
### Effect Group
Effect management commands.
| Command | Description |
|---------|-------------|
| `list-available` | List all available effects |
| `info` | Show details about an effect |
| `add` | Add an effect to a track |
| `remove` | Remove an effect by index |
| `set` | Set an effect parameter |
| `list` | List effects on a track |
### Selection
Selection management commands.
| Command | Description |
|---------|-------------|
| `set` | Set selection range |
| `all` | Select all (entire project duration) |
| `none` | Clear selection |
| `info` | Show current selection |
### Label
Label/marker management commands.
| Command | Description |
|---------|-------------|
| `add` | Add a label at a time position |
| `remove` | Remove a label by index |
| `list` | List all labels |
### Media
Media file operations.
| Command | Description |
|---------|-------------|
| `probe` | Analyze an audio file |
| `check` | Check that all referenced audio files exist |
### Export Group
Export/render commands.
| Command | Description |
|---------|-------------|
| `presets` | List export presets |
| `preset-info` | Show preset details |
| `render` | Render the project to an audio file |
### Session Group
Session management commands.
| Command | Description |
|---------|-------------|
| `status` | Show session status |
| `undo` | Undo the last operation |
| `redo` | Redo the last undone operation |
| `history` | Show undo history |
## Examples
### Create a New Project
Create a new audacity project file.
```bash
cli-anything-audacity project new -o myproject.json
# Or with JSON output for programmatic use
cli-anything-audacity --json project new -o myproject.json
```
### Interactive REPL Session
Start an interactive session with undo/redo support.
```bash
cli-anything-audacity
# Enter commands interactively
# Use 'help' to see available commands
# Use 'undo' and 'redo' for history navigation
```
### Export Project
Export the project to a final output format.
```bash
cli-anything-audacity --project myproject.json export render output.pdf --overwrite
```
## 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-audacity project info -p project.json
# JSON output for agents
cli-anything-audacity --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
1.0.0
+241
View File
@@ -0,0 +1,241 @@
---
name: "cli-anything-blender"
description: >-
Command-line interface for Blender - A stateful command-line interface for 3D scene editing, following the same patterns as the GIMP CLI ...
---
# cli-anything-blender
A stateful command-line interface for 3D scene editing, following the same patterns as the GIMP CLI harness. Uses a JSON scene description format with bpy script generation for actual Blender rendering.
## Installation
This CLI is installed as part of the cli-anything-blender package:
```bash
pip install cli-anything-blender
```
**Prerequisites:**
- Python 3.10+
- blender (>= 4.2) must be installed on your system
## Usage
### Basic Commands
```bash
# Show help
cli-anything-blender --help
# Start interactive REPL mode
cli-anything-blender
# Create a new project
cli-anything-blender project new -o project.json
# Run with JSON output (for agent consumption)
cli-anything-blender --json project info -p project.json
```
### REPL Mode
When invoked without a subcommand, the CLI enters an interactive REPL session:
```bash
cli-anything-blender
# Enter commands interactively with tab-completion and history
```
## Command Groups
### Scene
Scene management commands.
| Command | Description |
|---------|-------------|
| `new` | Create a new scene |
| `open` | Open an existing scene |
| `save` | Save the current scene |
| `info` | Show scene information |
| `profiles` | List available scene profiles |
| `json` | Print raw scene JSON |
### Object Group
3D object management commands.
| Command | Description |
|---------|-------------|
| `add` | Add a 3D primitive object |
| `remove` | Remove an object by index |
| `duplicate` | Duplicate an object |
| `transform` | Transform an object (translate, rotate, scale) |
| `set` | Set an object property (name, visible, location, rotation, scale, parent) |
| `list` | List all objects |
| `get` | Get detailed info about an object |
### Material
Material management commands.
| Command | Description |
|---------|-------------|
| `create` | Create a new material |
| `assign` | Assign a material to an object |
| `set` | Set a material property (color, metallic, roughness, specular, alpha, etc.) |
| `list` | List all materials |
| `get` | Get detailed info about a material |
### Modifier Group
Modifier management commands.
| Command | Description |
|---------|-------------|
| `list-available` | List all available modifiers |
| `info` | Show details about a modifier |
| `add` | Add a modifier to an object |
| `remove` | Remove a modifier by index |
| `set` | Set a modifier parameter |
| `list` | List modifiers on an object |
### Camera
Camera management commands.
| Command | Description |
|---------|-------------|
| `add` | Add a camera to the scene |
| `set` | Set a camera property |
| `set-active` | Set the active camera |
| `list` | List all cameras |
### Light
Light management commands.
| Command | Description |
|---------|-------------|
| `add` | Add a light to the scene |
| `set` | Set a light property |
| `list` | List all lights |
### Animation
Animation and keyframe commands.
| Command | Description |
|---------|-------------|
| `keyframe` | Set a keyframe on an object |
| `remove-keyframe` | Remove a keyframe from an object |
| `frame-range` | Set the animation frame range |
| `fps` | Set the animation FPS |
| `list-keyframes` | List keyframes for an object |
### Render Group
Render settings and output commands.
| Command | Description |
|---------|-------------|
| `settings` | Configure render settings |
| `info` | Show current render settings |
| `presets` | List available render presets |
| `execute` | Render the scene (generates bpy script) |
| `script` | Generate bpy script without rendering |
### Session
Session management commands.
| Command | Description |
|---------|-------------|
| `status` | Show session status |
| `undo` | Undo the last operation |
| `redo` | Redo the last undone operation |
| `history` | Show undo history |
## Examples
### Create a New Project
Create a new blender project file.
```bash
cli-anything-blender project new -o myproject.json
# Or with JSON output for programmatic use
cli-anything-blender --json project new -o myproject.json
```
### Interactive REPL Session
Start an interactive session with undo/redo support.
```bash
cli-anything-blender
# Enter commands interactively
# Use 'help' to see available commands
# Use 'undo' and 'redo' for history navigation
```
## 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-blender project info -p project.json
# JSON output for agents
cli-anything-blender --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. **MANDATORY: Use absolute paths** for all file operations (rendering, project files). Relative paths are prone to failure in background execution.
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
1.0.0
+213
View File
@@ -0,0 +1,213 @@
---
name: "cli-anything-browser"
description: "Browser automation CLI using DOMShell MCP server. Maps Chrome's Accessibility Tree to a virtual filesystem for agent-native navigation."
---
# cli-anything-browser
A command-line interface for browser automation using [DOMShell](https://github.com/apireno/DOMShell)'s MCP server. Navigate web pages using filesystem commands: `ls`, `cd`, `cat`, `grep`, `click`.
## Installation
### Prerequisites
1. **Node.js and npx** (for DOMShell MCP server):
```bash
# Install Node.js from https://nodejs.org/
npx --version
```
2. **Chrome/Chromium** with [DOMShell extension](https://chromewebstore.google.com/detail/domshell-browser-filesy/okcliheamhmijccjknkkplploacoidnp):
- Install extension in Chrome
- Ensure Chrome is running before using CLI
3. **Python 3.10+**
### Install CLI
```bash
cd browser/agent-harness
pip install -e .
```
## Command Groups
### `page` — Page Navigation
- `page open <url>` — Navigate to URL
- `page reload` — Reload current page
- `page back` — Navigate back in history
- `page forward` — Navigate forward in history
- `page info` — Show current page info
### `fs` — Filesystem Commands (Accessibility Tree)
- `fs ls [path]` — List elements at path
- `fs cd <path>` — Change directory
- `fs cat [path]` — Read element content
- `fs grep <pattern> [path]` — Search for text pattern
- `fs pwd` — Print working directory
### `act` — Action Commands
- `act click <path>` — Click an element
- `act type <path> <text>` — Type text into input
### `session` — Session Management
- `session status` — Show session state
- `session daemon-start` — Start persistent daemon mode
- `session daemon-stop` — Stop daemon mode
## Usage Examples
### Basic Navigation
```bash
# Open a page
cli-anything-browser page open https://example.com
# Explore structure
cli-anything-browser fs ls /
cli-anything-browser fs cd /main
cli-anything-browser fs ls
# Go back to root
cli-anything-browser fs cd /
```
### Search and Click
```bash
cli-anything-browser fs grep "Login"
cli-anything-browser act click /main/button[0]
```
### Form Fill
```bash
cli-anything-browser act type /main/input[0] "user@example.com"
cli-anything-browser act click /main/button[0]
```
### JSON Output
```bash
cli-anything-browser --json fs ls /
```
### Daemon Mode (Faster Interactive Use)
```bash
# Start persistent connection
cli-anything-browser session daemon-start
# Run commands (uses persistent connection)
cli-anything-browser fs ls /
cli-anything-browser fs cd /main
# Stop daemon when done
cli-anything-browser session daemon-stop
```
### Interactive REPL
```bash
cli-anything-browser
```
## Path Syntax
DOMShell uses a filesystem-like path for the Accessibility Tree:
```
/ — Root (document)
/main — Main landmark
/main/div[0] — First div in main
/main/div[0]/button[2] — Third button in first div
```
- Array indices are **0-based**: `button[0]` is the first button
- Use `..` to go up one level
- Use `/` for root
## Agent-Specific Guidance
### JSON Output for Parsing
All commands support `--json` flag for machine-readable output:
```bash
cli-anything-browser --json fs ls /
```
Returns:
```json
{
"path": "/",
"entries": [
{"name": "main", "role": "landmark", "path": "/main"}
]
}
```
### Error Handling
The CLI provides clear error messages for common issues:
- **npx not found**: Install Node.js from https://nodejs.org/
- **DOMShell not found**: Run `npx @apireno/domshell --version`
- **MCP call failed**: Install DOMShell Chrome extension
Check `is_available()` return value before running commands.
### Daemon Mode for Efficiency
For agent workflows with multiple commands, use daemon mode:
1. Start daemon: `cli-anything-browser session daemon-start`
2. Run commands: Each command reuses the MCP connection
3. Stop daemon: `cli-anything-browser session daemon-stop`
This avoids the 1-3 second cold start overhead for each command.
## Links
- [DOMShell GitHub](https://github.com/apireno/DOMShell)
- [CLI-Anything](https://github.com/HKUDS/CLI-Anything)
- [Issue #90](https://github.com/HKUDS/CLI-Anything/issues/90)
## Security Considerations
**IMPORTANT**: When using this CLI with AI agents, be aware of the following security considerations:
### URL Restrictions
The browser harness validates all URLs before navigation:
- **Explicit scheme required**: URLs must include `http://` or `https://` scheme (scheme-less URLs like `example.com` are rejected)
- **Blocked schemes**: `file://`, `javascript://`, `data://`, `vbscript://`, `about://`, `chrome://`, and browser-internal schemes
- **Allowed schemes**: `http://` and `https://` only (configurable via `CLI_ANYTHING_BROWSER_ALLOWED_SCHEMES`)
- **Private network blocking**: Optional via `CLI_ANYTHING_BROWSER_BLOCK_PRIVATE=true` (disabled by default)
### DOM Content Risks
The Accessibility Tree includes all visible and hidden elements on a page. Malicious websites could:
- Craft ARIA labels with manipulative text (e.g., "Ignore previous instructions")
- Use aria-hidden elements to inject content not visible to users
- Create confusing DOM structures that mislead navigation
**Mitigation**: When interacting with untrusted websites, consider:
1. Using the `--json` flag for structured output that's easier to parse safely
2. Sanitizing or filtering DOM content before including it in prompts
3. Limiting navigation to trusted domains
### Private Network Access
By default, the browser can access localhost and private networks (192.168.x.x, 10.x.x.x, etc.). To block:
```bash
export CLI_ANYTHING_BROWSER_BLOCK_PRIVATE=true
cli-anything-browser page open http://localhost:8080 # Will be blocked
```
### Session Isolation
Multiple browser sessions share the same Chrome instance. Cookies and authentication state may persist across sessions. For sensitive operations, consider:
1. Using Chrome's guest mode or incognito
2. Clearing cookies between sessions
3. Using separate Chrome profiles for different security contexts
+116
View File
@@ -0,0 +1,116 @@
---
name: "cli-anything-chromadb"
description: >-
Command-line interface for ChromaDB - A stateless CLI for managing vector database collections, documents, and semantic search. Designed for AI agents and automation via the ChromaDB HTTP API v2.
---
# cli-anything-chromadb
A stateless command-line interface for ChromaDB vector database, built on the HTTP API v2. Designed for AI agents and power users who need to manage collections, documents, and run semantic queries without a browser UI.
## Installation
This CLI is installed as part of the cli-anything-chromadb package:
```bash
pip install cli-anything-chromadb
```
**Prerequisites:**
- Python 3.10+
- ChromaDB server running at localhost:8000 (or specify via --host)
## Usage
### Basic Commands
```bash
# Show help
cli-anything-chromadb --help
# Start interactive REPL mode
cli-anything-chromadb
# Check server health
cli-anything-chromadb --json server heartbeat
# List all collections
cli-anything-chromadb --json collection list
# Semantic search
cli-anything-chromadb --json query search --collection hub_knowledge --text "How to deploy"
```
### REPL Mode
When invoked without a subcommand, the CLI enters an interactive REPL session:
```bash
cli-anything-chromadb
# Enter commands interactively with tab-completion and history
```
## Command Groups
### server
Server health and version commands.
| Command | Description |
|---------|-------------|
| `heartbeat` | Check ChromaDB server health |
| `version` | Get ChromaDB server version |
### collection
Manage ChromaDB collections.
| Command | Description |
|---------|-------------|
| `list` | List all collections |
| `create --name NAME` | Create a new collection |
| `delete --name NAME` | Delete a collection |
| `info NAME` | Get collection info |
### document
Manage documents in collections.
| Command | Description |
|---------|-------------|
| `add --collection C --id ID --document TEXT` | Add document(s) |
| `get --collection C` | Get documents |
| `delete --collection C --id ID` | Delete document(s) |
| `count --collection C` | Count documents |
### query
Semantic search against collections.
| Command | Description |
|---------|-------------|
| `search --collection C --text T` | Semantic search |
## 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-chromadb server heartbeat
# JSON output for agents
cli-anything-chromadb --json server heartbeat
```
## 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 `--host`** to connect to non-default ChromaDB instances
## Version
1.0.0
+301
View File
@@ -0,0 +1,301 @@
---
name: "cli-anything-cloudanalyzer"
description: "Command-line interface for CloudAnalyzer — Agent-friendly harness for CloudAnalyzer, a QA platform for mapping, localization, and perception outputs. Supports 27 commands across 8 groups: point cloud evaluation, trajectory evaluation, ground segmentation QA, config-driven quality gates, baseline evolution, processing, visualization, and interactive REPL."
---
# cli-anything-cloudanalyzer
Agent-friendly command-line harness for [CloudAnalyzer](https://github.com/rsasaki0109/CloudAnalyzer) — a QA platform for mapping, localization, and perception point cloud outputs.
**27 commands** across 8 groups.
## Installation
```bash
pip install cli-anything-cloudanalyzer
```
**Prerequisites:**
- Python 3.10+
- CloudAnalyzer: `pip install cloudanalyzer`
## Global Options
```bash
cli-anything-cloudanalyzer [--project FILE] [--json] COMMAND [ARGS]...
```
| Option | Description |
|---|---|
| `-p, --project TEXT` | Path to project JSON file |
| `--json` | Output results as JSON (for agent consumption) |
## Command Groups
### 1. evaluate — Point Cloud Evaluation (6 commands)
#### evaluate run
Evaluate a point cloud against a reference (Chamfer, F1, AUC, Hausdorff).
```bash
cli-anything-cloudanalyzer evaluate run source.pcd reference.pcd
cli-anything-cloudanalyzer --json evaluate run source.pcd reference.pcd
```
Options: `--plot TEXT`, `--threshold FLOAT`
#### evaluate compare
Compare two point clouds with optional registration.
```bash
cli-anything-cloudanalyzer evaluate compare src.pcd tgt.pcd --register gicp
```
Options: `--register TEXT` (icp/gicp/none)
#### evaluate diff
Quick distance statistics between two point clouds.
```bash
cli-anything-cloudanalyzer evaluate diff a.pcd b.pcd --threshold 0.1
```
#### evaluate batch
Batch evaluation of multiple point clouds against a reference.
```bash
cli-anything-cloudanalyzer --json evaluate batch results/ reference.pcd --min-auc 0.95
```
Options: `--min-auc FLOAT`, `--max-chamfer FLOAT`
#### evaluate ground
Evaluate ground segmentation quality (precision, recall, F1, IoU).
```bash
cli-anything-cloudanalyzer --json evaluate ground est_ground.pcd est_ng.pcd ref_ground.pcd ref_ng.pcd --min-f1 0.9
```
Options: `--voxel-size FLOAT`, `--min-precision FLOAT`, `--min-recall FLOAT`, `--min-f1 FLOAT`, `--min-iou FLOAT`
#### evaluate pipeline
Filter, downsample, evaluate in one command.
```bash
cli-anything-cloudanalyzer evaluate pipeline input.pcd reference.pcd -o output.pcd
```
---
### 2. trajectory — Trajectory Evaluation (3 commands)
#### trajectory evaluate
Evaluate estimated vs reference trajectory (ATE, RPE, drift, lateral, longitudinal).
```bash
cli-anything-cloudanalyzer --json trajectory evaluate est.csv gt.csv --max-ate 0.5 --max-lateral 0.3
```
Options: `--max-ate FLOAT`, `--max-rpe FLOAT`, `--max-drift FLOAT`, `--min-coverage FLOAT`, `--max-lateral FLOAT`, `--max-longitudinal FLOAT`, `--align-origin`, `--align-rigid`
#### trajectory batch
Batch trajectory evaluation.
```bash
cli-anything-cloudanalyzer trajectory batch runs/ --reference-dir gt/ --max-drift 1.0
```
#### trajectory run-evaluate
Integrated map + trajectory evaluation.
```bash
cli-anything-cloudanalyzer trajectory run-evaluate map.pcd map_ref.pcd traj.csv traj_ref.csv
```
Options: `--min-auc FLOAT`, `--max-ate FLOAT`
---
### 3. check — Config-Driven Quality Gate (2 commands)
#### check run
Run unified QA from a config file.
```bash
cli-anything-cloudanalyzer --json check run cloudanalyzer.yaml
```
Options: `--output-json TEXT`
#### check init
Generate a starter config file.
```bash
cli-anything-cloudanalyzer check init cloudanalyzer.yaml --profile integrated
```
Options: `--profile TEXT` (mapping/localization/perception/integrated), `--force`
---
### 4. baseline — Baseline Evolution (3 commands)
#### baseline decision
Decide whether to promote, keep, or reject a candidate baseline.
```bash
cli-anything-cloudanalyzer --json baseline decision qa/summary.json --history-dir qa/history/
```
Options: `--history TEXT` (repeatable), `--history-dir TEXT`, `--output-json TEXT`
#### baseline save
Save a QA summary to the history directory.
```bash
cli-anything-cloudanalyzer baseline save qa/summary.json --history-dir qa/history/ --keep 10
```
Options: `--history-dir TEXT`, `--label TEXT`, `--keep INTEGER`
#### baseline list
List saved baselines.
```bash
cli-anything-cloudanalyzer --json baseline list --history-dir qa/history/
```
---
### 5. process — Point Cloud Processing (6 commands)
#### process downsample
Voxel grid downsampling.
```bash
cli-anything-cloudanalyzer process downsample cloud.pcd -o down.pcd -v 0.05
```
#### process sample
Random point sampling.
```bash
cli-anything-cloudanalyzer process sample cloud.pcd -o sampled.pcd -n 10000
```
#### process filter
Statistical outlier removal.
```bash
cli-anything-cloudanalyzer process filter cloud.pcd -o filtered.pcd
```
#### process split
Split point cloud into grid tiles (writes metadata.yaml).
```bash
cli-anything-cloudanalyzer process split large.pcd -o tiles/ -g 100
```
#### process merge
Merge multiple point clouds.
```bash
cli-anything-cloudanalyzer process merge a.pcd b.pcd -o merged.pcd
```
#### process convert
Convert between point cloud formats.
```bash
cli-anything-cloudanalyzer process convert input.las -o output.pcd
```
---
### 6. inspect — Visualization (3 commands)
#### inspect view
Open a point cloud viewer.
```bash
cli-anything-cloudanalyzer inspect view cloud.pcd
```
#### inspect web
Interactive browser inspection.
```bash
cli-anything-cloudanalyzer inspect web map.pcd ref.pcd --heatmap
```
#### inspect web-export
Export a static HTML inspection bundle.
```bash
cli-anything-cloudanalyzer inspect web-export map.pcd ref.pcd -o bundle/
```
---
### 7. info — Metadata (2 commands)
#### info show
Show point cloud metadata.
```bash
cli-anything-cloudanalyzer --json info show cloud.pcd
```
#### info version
Show CloudAnalyzer version.
---
### 8. session — Session Management (2 commands)
#### session new
Create a new harness project JSON file.
```bash
cli-anything-cloudanalyzer session new -o project.json -n my-run
```
#### session history
Show recent operations for the project given with `-p` / `--project`.
```bash
cli-anything-cloudanalyzer --project project.json session history --last 20
```
---
## Typical Agent Workflows
### Workflow 1: Evaluate and gate a point cloud
```bash
cli-anything-cloudanalyzer --json evaluate run output.pcd reference.pcd
```
### Workflow 2: Config-driven QA pipeline
```bash
cli-anything-cloudanalyzer check init cloudanalyzer.yaml --profile integrated
cli-anything-cloudanalyzer --json check run cloudanalyzer.yaml
```
### Workflow 3: Baseline management
```bash
cli-anything-cloudanalyzer --json check run cloudanalyzer.yaml --output-json qa/summary.json
cli-anything-cloudanalyzer baseline save qa/summary.json --history-dir qa/history/
cli-anything-cloudanalyzer --json baseline decision qa/summary.json --history-dir qa/history/
```
### Workflow 4: Ground segmentation QA
```bash
cli-anything-cloudanalyzer --json evaluate ground \
est_ground.pcd est_ng.pcd ref_ground.pcd ref_ng.pcd --min-f1 0.9
```

Some files were not shown because too many files have changed in this diff Show More