Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .github/workflows/pr-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: PR CI

on:
workflow_dispatch:
pull_request:
branches:
- main
paths:
- '*/agent-harness/**'
- 'browser/**'
- 'cli-anything-plugin/repl_skin.py'
- 'README.md'
- '.github/workflows/pr-ci.yml'

jobs:
verify:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Install browser harness deps
run: |
python -m pip install --upgrade pip
python -m pip install -e browser/agent-harness[dev]

- name: Compile repl_skin.py files
run: |
python - <<'PY'
from pathlib import Path
import py_compile

root = Path('.')
paths = sorted(root.glob('**/agent-harness/cli_anything/**/utils/repl_skin.py'))
paths.append(root / 'cli-anything-plugin' / 'repl_skin.py')
seen = []
for path in paths:
if path.exists() and path not in seen:
seen.append(path)
for path in seen:
py_compile.compile(str(path), doraise=True)
print(f'compiled {len(seen)} repl_skin files')
PY

- name: Run browser unit tests
working-directory: browser/agent-harness
run: python -m pytest -q cli_anything/browser/tests/test_core.py
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ CLI-Anything: Bridging the Gap Between AI Agents and the World's Software</stron

**One Command Line**: Make any software agent-ready for Pi, OpenClaw, nanobot, Cursor, Claude Code, etc.&nbsp;&nbsp;[**中文文档**](README_CN.md) | [**日本語ドキュメント**](README_JA.md)

**🛠️ Maintenance**: REPL skin canonical source + sync/verification workflow lives in [`docs/repl-skin-maintenance.md`](docs/repl-skin-maintenance.md).

<p align="center">
<img src="assets/cli-typing.gif" alt="CLI-Anything typing demo" width="800">
</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from cli_anything.<software>.utils.repl_skin import ReplSkin

skin = ReplSkin("shotcut", version="1.0.0")
skin.print_banner()
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")
Expand Down Expand Up @@ -97,18 +97,31 @@ 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", "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
Expand Down Expand Up @@ -142,42 +155,18 @@ def _c(self, code: str, text: str) -> str:
# ── 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 = ""

print(top)
print(_box_line(title))
print(_box_line(ver))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
"""Print a compact startup banner."""
title = f"{self.display_name} v{self.version}"
tip = "Type help for commands, quit to exit"
print(title)
print(tip)
print()

# ── Prompt ────────────────────────────────────────────────────────

def prompt(self, project_name: str = "", modified: bool = False,
context: str = "") -> str:
"""Build a styled prompt string for prompt_toolkit or input().
"""Build a plain prompt string for interactive use.

Args:
project_name: Current project name (empty if none open).
Expand All @@ -187,28 +176,15 @@ def prompt(self, project_name: str = "", modified: bool = False,
Returns:
Formatted prompt string.
"""
parts = []

# Icon
if self._color:
parts.append(f"{_CYAN}◆{_RESET} ")
else:
parts.append("> ")
parts = [self.software]

# 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, " ❯ "))
parts.append(f"[{ctx}{mod}]")

return "".join(parts)
parts.append(">")
return " ".join(parts) + " "

def prompt_tokens(self, project_name: str = "", modified: bool = False,
context: str = ""):
Expand All @@ -219,11 +195,7 @@ def prompt_tokens(self, project_name: str = "", modified: bool = False,
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))
tokens = [("class:software", self.software)]

if project_name or context:
ctx = context or project_name
Expand All @@ -232,7 +204,7 @@ def prompt_tokens(self, project_name: str = "", modified: bool = False,
tokens.append(("class:context", f"{ctx}{mod}"))
tokens.append(("class:bracket", "]"))

tokens.append(("class:arrow", " "))
tokens.append(("class:arrow", " > "))

return tokens

Expand Down Expand Up @@ -393,24 +365,22 @@ def pad(text: str, width: int) -> str:
# ── Help display ──────────────────────────────────────────────────

def help(self, commands: dict[str, str]):
"""Print a formatted help listing.
"""Print a compact help listing.

Args:
commands: Dict of command -> description pairs.
"""
self.section("Commands")
print("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(f" {cmd:<{max_cmd}} {desc}")
print()

# ── Goodbye ───────────────────────────────────────────────────────

def print_goodbye(self):
"""Print a styled goodbye message."""
print(f"\n {_ICON_SMALL} {self._c(_GRAY, 'Goodbye!')}\n")
"""Print a compact goodbye message."""
print("Goodbye!\n")

# ── Prompt toolkit session factory ────────────────────────────────

Expand Down
Loading