diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml new file mode 100644 index 0000000000..021041f305 --- /dev/null +++ b/.github/workflows/pr-ci.yml @@ -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 diff --git a/README.md b/README.md index 47cdbf52eb..c82ce26098 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ CLI-Anything: Bridging the Gap Between AI Agents and the World's Software CLI-Anything typing demo

diff --git a/adguardhome/agent-harness/cli_anything/adguardhome/utils/repl_skin.py b/adguardhome/agent-harness/cli_anything/adguardhome/utils/repl_skin.py index 47260bebd0..6f918f96a2 100644 --- a/adguardhome/agent-harness/cli_anything/adguardhome/utils/repl_skin.py +++ b/adguardhome/agent-harness/cli_anything/adguardhome/utils/repl_skin.py @@ -7,7 +7,7 @@ from cli_anything..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") @@ -97,7 +97,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,10 +105,23 @@ def __init__(self, software: str, version: str = "1.0.0", version: CLI version string. history_file: Path for persistent command history. Defaults to ~/.cli-anything-/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//utils/repl_skin.py (this file) + # cli_anything//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 @@ -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). @@ -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 = ""): @@ -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 @@ -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 @@ -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 ──────────────────────────────── diff --git a/anygen/agent-harness/cli_anything/anygen/utils/repl_skin.py b/anygen/agent-harness/cli_anything/anygen/utils/repl_skin.py index a444e04589..6f918f96a2 100644 --- a/anygen/agent-harness/cli_anything/anygen/utils/repl_skin.py +++ b/anygen/agent-harness/cli_anything/anygen/utils/repl_skin.py @@ -7,7 +7,7 @@ from cli_anything..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") @@ -47,7 +47,6 @@ "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 @@ -98,7 +97,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,10 +105,23 @@ def __init__(self, software: str, version: str = "1.0.0", version: CLI version string. history_file: Path for persistent command history. Defaults to ~/.cli-anything-/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//utils/repl_skin.py (this file) + # cli_anything//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 @@ -143,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). @@ -188,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 = ""): @@ -220,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 @@ -233,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 @@ -394,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 ──────────────────────────────── @@ -494,7 +463,6 @@ def toolbar(): "\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 } diff --git a/audacity/agent-harness/cli_anything/audacity/utils/repl_skin.py b/audacity/agent-harness/cli_anything/audacity/utils/repl_skin.py index 47260bebd0..6f918f96a2 100644 --- a/audacity/agent-harness/cli_anything/audacity/utils/repl_skin.py +++ b/audacity/agent-harness/cli_anything/audacity/utils/repl_skin.py @@ -7,7 +7,7 @@ from cli_anything..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") @@ -97,7 +97,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,10 +105,23 @@ def __init__(self, software: str, version: str = "1.0.0", version: CLI version string. history_file: Path for persistent command history. Defaults to ~/.cli-anything-/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//utils/repl_skin.py (this file) + # cli_anything//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 @@ -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). @@ -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 = ""): @@ -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 @@ -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 @@ -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 ──────────────────────────────── diff --git a/blender/agent-harness/cli_anything/blender/utils/repl_skin.py b/blender/agent-harness/cli_anything/blender/utils/repl_skin.py index 47260bebd0..6f918f96a2 100644 --- a/blender/agent-harness/cli_anything/blender/utils/repl_skin.py +++ b/blender/agent-harness/cli_anything/blender/utils/repl_skin.py @@ -7,7 +7,7 @@ from cli_anything..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") @@ -97,7 +97,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,10 +105,23 @@ def __init__(self, software: str, version: str = "1.0.0", version: CLI version string. history_file: Path for persistent command history. Defaults to ~/.cli-anything-/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//utils/repl_skin.py (this file) + # cli_anything//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 @@ -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). @@ -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 = ""): @@ -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 @@ -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 @@ -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 ──────────────────────────────── diff --git a/browser/agent-harness/cli_anything/browser/browser_cli.py b/browser/agent-harness/cli_anything/browser/browser_cli.py index 54fbc565c0..3ec64dccf1 100644 --- a/browser/agent-harness/cli_anything/browser/browser_cli.py +++ b/browser/agent-harness/cli_anything/browser/browser_cli.py @@ -25,6 +25,7 @@ from cli_anything.browser.core import page as page_mod from cli_anything.browser.core import fs as fs_mod from cli_anything.browser.utils import domshell_backend as backend +from cli_anything.browser.utils.tool_result import tool_result_body_text, tool_result_error_text, tool_result_has_error # Global state _session: Optional[Session] = None @@ -36,22 +37,32 @@ def get_session() -> Session: global _session if _session is None: - _session = Session() + if backend.daemon_started(): + _session = Session.load_persisted() + else: + _session = Session() return _session -def output(data, message: str = ""): +def output(data, message: str = "", body: Optional[str] = None): if _json_output: click.echo(json.dumps(data, indent=2, default=str)) + return + + if message: + click.echo(message) + + if body is not None: + if body: + click.echo(body) + return + + if isinstance(data, dict): + _print_dict(data) + elif isinstance(data, list): + _print_list(data) else: - if message: - click.echo(message) - if isinstance(data, dict): - _print_dict(data) - elif isinstance(data, list): - _print_list(data) - else: - click.echo(str(data)) + click.echo(str(data)) def _print_dict(d: dict, indent: int = 0): @@ -77,6 +88,21 @@ def _print_list(items: list, indent: int = 0): click.echo(f"{prefix}- {item}") +def _format_page_info(sess: Session) -> str: + url = sess.current_url or "(no page loaded)" + return f"URL: {url}\nWorking dir: {sess.working_dir}" + + +def _format_session_status(status: dict) -> str: + return ( + f"URL: {status.get('current_url', '(no page loaded)')}\n" + f"Working dir: {status.get('working_dir', '/')}\n" + f"History: {status.get('history_length', 0)}\n" + f"Forward: {status.get('forward_stack_length', 0)}\n" + f"Daemon: {'on' if status.get('daemon_mode') else 'off'}" + ) + + def handle_error(func): def wrapper(*args, **kwargs): try: @@ -136,6 +162,7 @@ def cli(ctx, use_json, use_daemon): if use_daemon: try: backend.start_daemon() + _session.persist_state = True _session.enable_daemon() if not _json_output: click.echo("Daemon mode: persistent MCP connection active") @@ -164,7 +191,10 @@ def page_open(url): """Open a URL in Chrome.""" sess = get_session() result = page_mod.open_page(sess, url) - output(result, f"Opened: {url}") + if tool_result_has_error(result): + output(result, tool_result_error_text(result, "open failed"), body="") + else: + output(result, f"Opened: {url}", body="") @page.command("reload") @@ -173,7 +203,10 @@ def page_reload(): """Reload the current page.""" sess = get_session() result = page_mod.reload_page(sess) - output(result, "Page reloaded") + if tool_result_has_error(result): + output(result, tool_result_error_text(result, "reload failed"), body="") + else: + output(result, "Page reloaded", body="") @page.command("back") @@ -182,10 +215,10 @@ def page_back(): """Navigate back in history.""" sess = get_session() result = page_mod.go_back(sess) - if "error" in result: - output(result, result["error"]) + if tool_result_has_error(result): + output(result, tool_result_error_text(result, "back failed"), body="") else: - output(result, "Navigated back") + output(result, "Navigated back", body="") @page.command("forward") @@ -194,10 +227,10 @@ def page_forward(): """Navigate forward in history.""" sess = get_session() result = page_mod.go_forward(sess) - if "error" in result: - output(result, result["error"]) + if tool_result_has_error(result): + output(result, tool_result_error_text(result, "forward failed"), body="") else: - output(result, "Navigated forward") + output(result, "Navigated forward", body="") @page.command("info") @@ -206,7 +239,7 @@ def page_info(): """Show current page information.""" sess = get_session() result = page_mod.get_page_info(sess) - output(result) + output(result, body=_format_page_info(sess)) # ── Filesystem Commands ────────────────────────────────────────── @@ -223,7 +256,9 @@ def fs_ls(path): """List elements at a path in the accessibility tree.""" sess = get_session() result = fs_mod.list_elements(sess, path) - if _json_output: + if tool_result_has_error(result): + output(result, tool_result_error_text(result, "ls failed"), body="") + elif _json_output: output(result) else: entries = result.get("entries", []) @@ -246,10 +281,10 @@ def fs_cd(path): """Change directory in the accessibility tree.""" sess = get_session() result = fs_mod.change_directory(sess, path) - if "error" in result: - output(result, result["error"]) + if tool_result_has_error(result): + output(result, tool_result_error_text(result, "cd failed"), body="") else: - output(result, f"Changed to: {sess.working_dir}") + output(result, f"Changed to: {sess.working_dir}", body="") @fs.command("cat") @@ -259,7 +294,10 @@ def fs_cat(path): """Read element content from the accessibility tree.""" sess = get_session() result = fs_mod.read_element(sess, path) - output(result) + if tool_result_has_error(result): + output(result, tool_result_error_text(result, "cat failed"), body="") + else: + output(result, body=tool_result_body_text(result, "(empty)")) @fs.command("grep") @@ -270,7 +308,9 @@ def fs_grep(pattern, path): """Search for pattern in the accessibility tree.""" sess = get_session() result = fs_mod.grep_elements(sess, pattern, path) - if _json_output: + if tool_result_has_error(result): + output(result, tool_result_error_text(result, "grep failed"), body="") + elif _json_output: output(result) else: matches = result.get("matches", []) @@ -303,9 +343,12 @@ def act(): def act_click(path): """Click an element at the given path.""" sess = get_session() - use_daemon = sess.daemon_mode + use_daemon = sess.daemon_mode or backend.daemon_started() result = backend.click(path, use_daemon=use_daemon) - output(result, f"Clicked: {path}") + if tool_result_has_error(result): + output(result, tool_result_error_text(result, "click failed"), body="") + else: + output(result, f"Clicked: {path}", body="") @act.command("type") @@ -315,9 +358,12 @@ def act_click(path): def act_type(path, text): """Type text into an input element.""" sess = get_session() - use_daemon = sess.daemon_mode + use_daemon = sess.daemon_mode or backend.daemon_started() result = backend.type_text(path, text, use_daemon=use_daemon) - output(result, f"Typed into: {path}") + if tool_result_has_error(result): + output(result, tool_result_error_text(result, "type failed"), body="") + else: + output(result, f"Typed into: {path}", body="") # ── Session Commands ───────────────────────────────────────────── @@ -333,7 +379,8 @@ def session_status(): """Show current session status.""" sess = get_session() status = sess.status() - output(status) + status["daemon_mode"] = status.get("daemon_mode", False) or backend.daemon_started() + output(status, body=_format_session_status(status)) @session.command("daemon-start") @@ -342,10 +389,12 @@ def session_daemon_start(): """Start persistent daemon mode.""" try: backend.start_daemon() - get_session().enable_daemon() - output({"daemon": "started"}, "Daemon mode started") + sess = get_session() + sess.persist_state = True + sess.enable_daemon() + output({"daemon": "started"}, "Daemon mode started", body="") except RuntimeError as e: - output({"error": str(e)}, str(e)) + output({"error": str(e)}, str(e), body="") @session.command("daemon-stop") @@ -353,8 +402,10 @@ def session_daemon_start(): def session_daemon_stop(): """Stop persistent daemon mode.""" backend.stop_daemon() - get_session().disable_daemon() - output({"daemon": "stopped"}, "Daemon mode stopped") + sess = get_session() + sess.persist_state = True + sess.disable_daemon() + output({"daemon": "stopped"}, "Daemon mode stopped", body="") # ── REPL ───────────────────────────────────────────────────────── diff --git a/browser/agent-harness/cli_anything/browser/core/fs.py b/browser/agent-harness/cli_anything/browser/core/fs.py index 303cc50072..23637ba636 100644 --- a/browser/agent-harness/cli_anything/browser/core/fs.py +++ b/browser/agent-harness/cli_anything/browser/core/fs.py @@ -10,6 +10,8 @@ from typing import TYPE_CHECKING +from cli_anything.browser.utils.tool_result import tool_result_has_error + if TYPE_CHECKING: from cli_anything.browser.core.session import Session @@ -28,11 +30,23 @@ def list_elements(session: "Session", path: str = "") -> dict: Example: >>> list_elements(session, "/main") - {"path": "/main", "entries": [{"name": "button", "role": "button", ...}]} + {"path": "/", "entries": [{"name": "main", "role": "landmark", ...}]} """ target_path = path if path else session.working_dir - use_daemon = session.daemon_mode - return backend.ls(target_path, use_daemon=use_daemon) + use_daemon = session.daemon_mode or backend.daemon_started() + + # DOMShell ls can silently return an empty list for invalid paths. Preflight + # the target with cd, then restore the original working directory. + if target_path and target_path != "/": + cd_result = backend.cd(target_path, use_daemon=use_daemon) + if tool_result_has_error(cd_result): + return cd_result + + try: + return backend.ls(target_path, use_daemon=use_daemon) + finally: + if target_path and target_path != "/": + backend.cd(session.working_dir or "/", use_daemon=use_daemon) def change_directory(session: "Session", path: str) -> dict: @@ -68,11 +82,11 @@ def change_directory(session: "Session", path: str) -> dict: else: path = session.working_dir.rstrip("/") + "/" + path - use_daemon = session.daemon_mode + use_daemon = session.daemon_mode or backend.daemon_started() result = backend.cd(path, use_daemon=use_daemon) # Only update working_dir if backend succeeded - if isinstance(result, dict) and "error" not in result: - new_working_dir = result.get("path", path) + if isinstance(result, dict) and not tool_result_has_error(result): + new_working_dir = result.get("path") or result.get("working_dir") or path session.set_working_dir(new_working_dir) return result @@ -92,7 +106,7 @@ def read_element(session: "Session", path: str = "") -> dict: {"name": "Submit", "role": "button", "text": "Submit", ...} """ target_path = path if path else session.working_dir - use_daemon = session.daemon_mode + use_daemon = session.daemon_mode or backend.daemon_started() return backend.cat(target_path, use_daemon=use_daemon) @@ -112,13 +126,13 @@ def grep_elements(session: "Session", pattern: str, path: str = "") -> dict: {"matches": ["/main/button[0]", "/main/link[1]"]} """ target_path = path if path else session.working_dir - use_daemon = session.daemon_mode + use_daemon = session.daemon_mode or backend.daemon_started() # DOMShell's grep searches from the server-side CWD. To root the search # at the requested path, cd there first, grep, then restore. if target_path and target_path != "/": cd_result = backend.cd(target_path, use_daemon=use_daemon) - if hasattr(cd_result, 'isError') and cd_result.isError: + if tool_result_has_error(cd_result): return cd_result try: diff --git a/browser/agent-harness/cli_anything/browser/core/page.py b/browser/agent-harness/cli_anything/browser/core/page.py index 92a939eebb..9056299d74 100644 --- a/browser/agent-harness/cli_anything/browser/core/page.py +++ b/browser/agent-harness/cli_anything/browser/core/page.py @@ -14,6 +14,7 @@ from cli_anything.browser.utils import domshell_backend as backend from cli_anything.browser.utils.security import validate_url +from cli_anything.browser.utils.tool_result import tool_result_has_error def open_page(session: "Session", url: str) -> dict: @@ -41,10 +42,11 @@ def open_page(session: "Session", url: str) -> dict: if not is_valid: raise ValueError(error_msg) - use_daemon = session.daemon_mode + use_daemon = session.daemon_mode or backend.daemon_started() result = backend.open_url(url, use_daemon=use_daemon) - session.set_url(url) - session.set_working_dir("/") # Reset to root on new page + if not tool_result_has_error(result): + session.set_url(url) + session.set_working_dir("/") # Reset to root on new page return result @@ -61,7 +63,7 @@ def reload_page(session: "Session") -> dict: >>> reload_page(session) {"status": "reloaded", "url": "https://example.com"} """ - use_daemon = session.daemon_mode + use_daemon = session.daemon_mode or backend.daemon_started() result = backend.reload(use_daemon=use_daemon) return result @@ -79,7 +81,7 @@ def go_back(session: "Session") -> dict: >>> go_back(session) {"url": "https://previous.com", "status": "navigated"} """ - use_daemon = session.daemon_mode + use_daemon = session.daemon_mode or backend.daemon_started() result = backend.back(use_daemon=use_daemon) # Update session state if backend returned a URL @@ -102,7 +104,7 @@ def go_forward(session: "Session") -> dict: >>> go_forward(session) {"url": "https://next.com", "status": "navigated"} """ - use_daemon = session.daemon_mode + use_daemon = session.daemon_mode or backend.daemon_started() result = backend.forward(use_daemon=use_daemon) # Update session state if backend returned a URL diff --git a/browser/agent-harness/cli_anything/browser/core/session.py b/browser/agent-harness/cli_anything/browser/core/session.py index 3422f0373b..7514b404de 100644 --- a/browser/agent-harness/cli_anything/browser/core/session.py +++ b/browser/agent-harness/cli_anything/browser/core/session.py @@ -7,8 +7,10 @@ - Daemon mode status """ -from typing import Optional +import json from dataclasses import dataclass, field +from pathlib import Path +from typing import Optional @dataclass @@ -28,6 +30,53 @@ class Session: history: list[str] = field(default_factory=list) forward_stack: list[str] = field(default_factory=list) daemon_mode: bool = False + persist_state: bool = False + state_path: Optional[str] = None + + @classmethod + def load_persisted(cls, state_path: Optional[str] = None) -> "Session": + """Load a persisted session snapshot if present. + + The returned session is marked persistent so future mutations are written + back to disk. If no active daemon snapshot exists, returns a fresh session. + """ + session = cls(persist_state=True, state_path=state_path) + try: + path = session._resolve_state_path() + if not path.exists(): + return session + data = json.loads(path.read_text(encoding="utf-8")) + except Exception: + return session + + session.current_url = data.get("current_url", "") + session.working_dir = data.get("working_dir", "/") + session.history = list(data.get("history", [])) + session.forward_stack = list(data.get("forward_stack", [])) + session.daemon_mode = bool(data.get("daemon_mode", False)) + return session + + def _resolve_state_path(self) -> Path: + if self.state_path: + return Path(self.state_path) + return Path.home() / ".cli-anything-browser" / "session.json" + + def save_state(self) -> None: + """Persist the current session snapshot when enabled.""" + if not self.persist_state: + return + path = self._resolve_state_path() + path.parent.mkdir(parents=True, exist_ok=True) + payload = { + "current_url": self.current_url, + "working_dir": self.working_dir, + "history": self.history, + "forward_stack": self.forward_stack, + "daemon_mode": self.daemon_mode, + } + tmp_path = path.with_suffix(path.suffix + ".tmp") + tmp_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8") + tmp_path.replace(path) def set_url(self, url: str, record_history: bool = True) -> None: """Set the current URL and update history. @@ -40,6 +89,7 @@ def set_url(self, url: str, record_history: bool = True) -> None: self.history.append(self.current_url) self.forward_stack.clear() # Clear forward stack on new navigation self.current_url = url + self.save_state() def go_back(self) -> Optional[str]: """Navigate back in history. @@ -52,6 +102,7 @@ def go_back(self) -> Optional[str]: previous = self.history.pop() self.forward_stack.append(self.current_url) self.current_url = previous + self.save_state() return previous def go_forward(self) -> Optional[str]: @@ -65,6 +116,7 @@ def go_forward(self) -> Optional[str]: next_url = self.forward_stack.pop() self.history.append(self.current_url) self.current_url = next_url + self.save_state() return next_url def set_working_dir(self, path: str) -> None: @@ -74,14 +126,17 @@ def set_working_dir(self, path: str) -> None: path: New path (e.g., "/main/div[0]") """ self.working_dir = path + self.save_state() def enable_daemon(self) -> None: """Enable daemon mode for persistent MCP connection.""" self.daemon_mode = True + self.save_state() def disable_daemon(self) -> None: """Disable daemon mode.""" self.daemon_mode = False + self.save_state() def status(self) -> dict: """Get session status as a dict. diff --git a/browser/agent-harness/cli_anything/browser/tests/test_core.py b/browser/agent-harness/cli_anything/browser/tests/test_core.py index cb807a283a..66c0b22e64 100644 --- a/browser/agent-harness/cli_anything/browser/tests/test_core.py +++ b/browser/agent-harness/cli_anything/browser/tests/test_core.py @@ -6,11 +6,44 @@ python -m pytest cli_anything/browser/tests/test_core.py -v """ -import pytest from unittest.mock import AsyncMock, MagicMock, patch +import pytest + from cli_anything.browser.core.session import Session from cli_anything.browser.core import page, fs +from cli_anything.browser.utils.repl_skin import ReplSkin + + +@pytest.fixture(autouse=True) +def force_non_daemon(monkeypatch): + """Keep unit tests isolated from any real daemon running on the machine.""" + monkeypatch.setattr(fs.backend, "daemon_started", lambda: False) + + +class TestReplSkin: + """Test compact REPL prompt/help formatting.""" + + def test_prompt_is_plain_text(self): + skin = ReplSkin("browser", version="1.0.0") + assert skin.prompt(context="https://example.com /") == "browser [https://example.com /] > " + + def test_help_is_compact(self, capsys): + skin = ReplSkin("browser", version="1.0.0") + skin.help({"page": "open|reload", "fs": "ls|cd|cat|grep|pwd"}) + out = capsys.readouterr().out + assert out == "Commands:\n page open|reload\n fs ls|cd|cat|grep|pwd\n\n" + + def test_banner_and_goodbye_are_compact(self, capsys): + skin = ReplSkin("browser", version="1.0.0") + skin.print_banner() + skin.print_goodbye() + out = capsys.readouterr().out + assert "Browser v1.0.0" in out + assert "Type help for commands, quit to exit" in out + assert "Goodbye!" in out + assert "╭" not in out + assert "◆" not in out # ── Session Tests ──────────────────────────────────────────────── @@ -120,6 +153,22 @@ def test_status(self): assert status["forward_stack_length"] == 0 assert not status["daemon_mode"] + def test_persisted_session_round_trip(self, tmp_path): + """Persistent sessions can round-trip to disk.""" + state_file = tmp_path / "session.json" + sess = Session(persist_state=True, state_path=str(state_file)) + sess.enable_daemon() + sess.set_url("https://example.com") + sess.set_working_dir("/main") + sess.set_url("https://example.org") + + loaded = Session.load_persisted(str(state_file)) + assert loaded.daemon_mode is True + assert loaded.current_url == "https://example.org" + assert loaded.working_dir == "/main" + assert loaded.history == ["https://example.com"] + assert loaded.forward_stack == [] + # ── Page Module Tests ──────────────────────────────────────────── @@ -197,6 +246,202 @@ def test_get_page_info(self): assert result["url"] == "https://example.com" assert result["working_dir"] == "/main" + def test_open_page_rejects_error_payload(self): + """Open page should not mutate session on backend error payloads.""" + sess = Session() + sess.set_url("https://first.com") + sess.set_working_dir("/main") + + with patch("cli_anything.browser.core.page.backend.open_url") as mock_open: + mock_open.return_value = { + "content": [{"type": "text", "text": "Could not open"}], + "isError": False, + } + + result = page.open_page(sess, "https://second.com") + + assert sess.current_url == "https://first.com" + assert sess.working_dir == "/main" + assert result["isError"] is False + mock_open.assert_called_once_with("https://second.com", use_daemon=False) + + def test_act_click_rejects_error_payload(self): + """CLI click should not claim success on backend error payloads.""" + from cli_anything.browser.browser_cli import cli + from click.testing import CliRunner + + runner = CliRunner() + with patch("cli_anything.browser.browser_cli.backend.click") as mock_click: + mock_click.return_value = { + "content": [{"type": "text", "text": "click: /main/does-not-exist: No such element"}], + "isError": False, + } + result = runner.invoke(cli, ["act", "click", "/main/does-not-exist"]) + assert result.exit_code == 0 + assert "Clicked:" not in result.output + assert "No such element" in result.output + + def test_act_type_rejects_error_payload(self): + """CLI type should not claim success on backend error payloads.""" + from cli_anything.browser.browser_cli import cli + from click.testing import CliRunner + + runner = CliRunner() + with patch("cli_anything.browser.browser_cli.backend.type_text") as mock_type: + mock_type.return_value = { + "content": [{"type": "text", "text": "type failed: input not found"}], + "isError": False, + } + result = runner.invoke(cli, ["act", "type", "/main/does-not-exist", "hello"]) + assert result.exit_code == 0 + assert "Typed into:" not in result.output + assert "input not found" in result.output + + def test_type_text_preflight_blocks_invalid_path(self): + """type_text should return fast on invalid paths instead of hanging.""" + from cli_anything.browser.utils import domshell_backend as backend + + with patch("cli_anything.browser.utils.domshell_backend.cat") as mock_cat, \ + patch("cli_anything.browser.utils.domshell_backend.asyncio.run") as mock_run: + mock_cat.return_value = { + "content": [{"type": "text", "text": "cat: /main/does-not-exist: No such file or directory"}], + "isError": False, + } + mock_run.side_effect = AssertionError("asyncio.run should not be called for invalid paths") + result = backend.type_text("/main/does-not-exist", "hello", use_daemon=True) + assert isinstance(result, dict) + assert "No such file or directory" in result["content"][0]["text"] + + def test_page_reload_rejects_error_payload(self): + """Reload should not claim success on backend error payloads.""" + from cli_anything.browser.browser_cli import cli + from click.testing import CliRunner + + runner = CliRunner() + with patch("cli_anything.browser.browser_cli.page_mod.reload_page") as mock_reload: + mock_reload.return_value = { + "content": [{"type": "text", "text": "reload failed: could not refresh"}], + "isError": False, + } + result = runner.invoke(cli, ["page", "reload"]) + assert result.exit_code == 0 + assert "Page reloaded" not in result.output + assert "reload failed" in result.output + assert "content:" not in result.output + assert "isError" not in result.output + + def test_page_back_rejects_error_payload(self): + """Back should not claim success on backend error payloads.""" + from cli_anything.browser.browser_cli import cli + from click.testing import CliRunner + + runner = CliRunner() + with patch("cli_anything.browser.browser_cli.page_mod.go_back") as mock_back: + mock_back.return_value = { + "content": [{"type": "text", "text": "back failed: No history"}], + "isError": False, + } + result = runner.invoke(cli, ["page", "back"]) + assert result.exit_code == 0 + assert "Navigated back" not in result.output + assert "No history" in result.output + assert "content:" not in result.output + assert "isError" not in result.output + + def test_fs_ls_rejects_error_payload(self): + """ls should not claim success when backend returns an error payload.""" + from cli_anything.browser.browser_cli import cli + from click.testing import CliRunner + + runner = CliRunner() + with patch("cli_anything.browser.browser_cli.fs_mod.list_elements") as mock_ls: + mock_ls.return_value = { + "content": [{"type": "text", "text": "ls failed: No such directory"}], + "isError": False, + } + result = runner.invoke(cli, ["fs", "ls", "/main"]) + assert result.exit_code == 0 + assert "No elements at" not in result.output + assert "No such directory" in result.output + assert "content:" not in result.output + assert "isError" not in result.output + + def test_fs_cat_rejects_error_payload(self): + """cat should show the backend error text instead of a fake success payload.""" + from cli_anything.browser.browser_cli import cli + from click.testing import CliRunner + + runner = CliRunner() + with patch("cli_anything.browser.browser_cli.fs_mod.read_element") as mock_cat: + mock_cat.return_value = { + "content": [{"type": "text", "text": "cat: /main/does-not-exist: No such file or directory"}], + "isError": False, + } + result = runner.invoke(cli, ["fs", "cat", "/main/does-not-exist"]) + assert result.exit_code == 0 + assert "No such file or directory" in result.output + assert "content:" not in result.output + assert "isError" not in result.output + + def test_page_info_is_concise(self): + """page info should render a concise summary instead of raw payload.""" + from cli_anything.browser.browser_cli import cli + from click.testing import CliRunner + + runner = CliRunner() + result = runner.invoke(cli, ["page", "info"]) + assert result.exit_code == 0 + assert "URL:" in result.output + assert "Working dir:" in result.output + assert "current_url" not in result.output + assert "working_dir" not in result.output + assert "content:" not in result.output + + def test_session_status_is_concise(self): + """session status should render a concise summary instead of raw payload.""" + from cli_anything.browser.browser_cli import cli + from click.testing import CliRunner + + runner = CliRunner() + result = runner.invoke(cli, ["session", "status"]) + assert result.exit_code == 0 + assert "URL:" in result.output + assert "Working dir:" in result.output + assert "History:" in result.output + assert "Forward:" in result.output + assert "Daemon:" in result.output + assert "current_url" not in result.output + assert "forward_stack_length" not in result.output + + def test_session_daemon_start_is_concise(self): + """daemon start should not print the payload dict in normal mode.""" + from cli_anything.browser.browser_cli import cli + from click.testing import CliRunner + + runner = CliRunner() + with patch("cli_anything.browser.browser_cli.backend.start_daemon"): + result = runner.invoke(cli, ["session", "daemon-start"]) + assert result.exit_code == 0 + assert "Daemon mode started" in result.output + assert "daemon:" not in result.output + + def test_change_directory_rejects_text_error_payload(self): + """cd should not update working_dir when backend returns an error text payload.""" + sess = Session() + sess.set_working_dir("/") + + with patch("cli_anything.browser.core.fs.backend.cd") as mock_cd: + mock_cd.return_value = { + "content": [{"type": "text", "text": "cd: main: No such directory"}], + "isError": False, + } + + result = fs.change_directory(sess, "/main") + + assert sess.working_dir == "/" + assert result["isError"] is False + mock_cd.assert_called_once_with("/main", use_daemon=False) + # ── Filesystem Module Tests ─────────────────────────────────────── @@ -208,7 +453,9 @@ def test_list_elements(self): sess = Session() sess.set_working_dir("/main") - with patch("cli_anything.browser.core.fs.backend.ls") as mock_ls: + with patch("cli_anything.browser.core.fs.backend.cd") as mock_cd, \ + patch("cli_anything.browser.core.fs.backend.ls") as mock_ls: + mock_cd.return_value = {"path": "/main", "status": "changed"} mock_ls.return_value = { "path": "/main", "entries": [{"name": "button", "role": "button", "path": "/main/button[0]"}] @@ -216,6 +463,7 @@ def test_list_elements(self): result = fs.list_elements(sess) + mock_cd.assert_any_call("/main", use_daemon=False) mock_ls.assert_called_once_with("/main", use_daemon=False) def test_list_elements_with_path(self): @@ -223,11 +471,14 @@ def test_list_elements_with_path(self): sess = Session() sess.set_working_dir("/main") - with patch("cli_anything.browser.core.fs.backend.ls") as mock_ls: + with patch("cli_anything.browser.core.fs.backend.cd") as mock_cd, \ + patch("cli_anything.browser.core.fs.backend.ls") as mock_ls: + mock_cd.return_value = {"path": "/div", "status": "changed"} mock_ls.return_value = {"path": "/div", "entries": []} result = fs.list_elements(sess, "/div") + mock_cd.assert_any_call("/div", use_daemon=False) mock_ls.assert_called_once_with("/div", use_daemon=False) def test_list_elements_empty_path_uses_working_dir(self): @@ -235,11 +486,14 @@ def test_list_elements_empty_path_uses_working_dir(self): sess = Session() sess.set_working_dir("/main") - with patch("cli_anything.browser.core.fs.backend.ls") as mock_ls: + with patch("cli_anything.browser.core.fs.backend.cd") as mock_cd, \ + patch("cli_anything.browser.core.fs.backend.ls") as mock_ls: + mock_cd.return_value = {"path": "/main", "status": "changed"} mock_ls.return_value = {"path": "/main", "entries": []} result = fs.list_elements(sess, "") + mock_cd.assert_any_call("/main", use_daemon=False) mock_ls.assert_called_once_with("/main", use_daemon=False) def test_change_directory_absolute_path(self): diff --git a/browser/agent-harness/cli_anything/browser/utils/domshell_backend.py b/browser/agent-harness/cli_anything/browser/utils/domshell_backend.py index eafc25767b..729f1f22c0 100644 --- a/browser/agent-harness/cli_anything/browser/utils/domshell_backend.py +++ b/browser/agent-harness/cli_anything/browser/utils/domshell_backend.py @@ -13,13 +13,25 @@ """ import asyncio +import json import os +import signal +import socket import subprocess import shutil +import sys +from pathlib import Path from typing import Any, Optional + from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client +from cli_anything.browser.utils.tool_result import ( + normalize_tool_result as _normalize_tool_result, + tool_result_has_error as _tool_result_has_error, +) + + # DOMShell MCP server command # The harness connects to a running DOMShell server via domshell-proxy (stdio bridge). # Configure via environment variables: @@ -46,10 +58,202 @@ def _build_server_args() -> list[str]: ] # Daemon mode: persistent MCP connection +# +# v1 used an in-process ClientSession only, which meant `session daemon-start` +# appeared to work but did not survive across CLI invocations. The upgraded +# design below runs a small background Unix-socket daemon so any later process +# can reuse the same browser session. _daemon_session: Optional[ClientSession] = None _daemon_read: Optional[Any] = None _daemon_write: Optional[Any] = None _daemon_client_context: Optional[Any] = None # Store stdio_client context manager +_daemon_process: Optional[subprocess.Popen] = None + + +def _daemon_runtime_dir() -> Path: + return Path.home() / ".cli-anything-browser" + + +def _daemon_socket_path() -> Path: + return _daemon_runtime_dir() / "daemon.sock" + + +def _daemon_pid_path() -> Path: + return _daemon_runtime_dir() / "daemon.pid" + + +def _ensure_daemon_runtime_dir() -> Path: + path = _daemon_runtime_dir() + path.mkdir(parents=True, exist_ok=True) + return path + + +def _daemon_is_socket_alive() -> bool: + """Return True when the Unix-socket daemon is reachable.""" + sock = _daemon_socket_path() + if not sock.exists(): + return False + try: + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client: + client.settimeout(0.5) + client.connect(str(sock)) + client.sendall(b'{"cmd":"ping"}\n') + data = b"" + while not data.endswith(b"\n"): + chunk = client.recv(4096) + if not chunk: + break + data += chunk + if not data: + return False + payload = json.loads(data.decode("utf-8", errors="replace")) + return payload.get("ok") is True + except OSError: + return False + except json.JSONDecodeError: + return False + + +def daemon_started() -> bool: + """Check if any daemon process is currently active.""" + return _daemon_is_socket_alive() or _daemon_session is not None + + +async def _daemon_request(payload: dict, timeout: float = 30.0) -> dict: + """Send one JSON request to the background daemon.""" + sock = _daemon_socket_path() + if not sock.exists(): + raise RuntimeError("Daemon socket not found") + + reader, writer = await asyncio.open_unix_connection(str(sock)) + try: + writer.write((json.dumps(payload) + "\n").encode("utf-8")) + await writer.drain() + + raw = await asyncio.wait_for(reader.readline(), timeout=timeout) + if not raw: + raise RuntimeError("Daemon disconnected before returning a result") + response = json.loads(raw.decode("utf-8", errors="replace")) + return response + finally: + writer.close() + try: + await writer.wait_closed() + except Exception: + pass + + +async def _daemon_server_main() -> None: + """Background daemon process that keeps one DOMShell MCP session alive.""" + socket_path = _daemon_socket_path() + runtime_dir = _ensure_daemon_runtime_dir() + pid_path = _daemon_pid_path() + + # Remove stale socket from a prior crash. + try: + socket_path.unlink() + except FileNotFoundError: + pass + + server_params = StdioServerParameters( + command=DEFAULT_SERVER_CMD, + args=_build_server_args(), + ) + + async with stdio_client(server_params) as (read, write): + async with ClientSession(read, write) as session: + await session.initialize() + + async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter): + try: + raw = await reader.readline() + if not raw: + return + request = json.loads(raw.decode("utf-8", errors="replace")) + cmd = request.get("cmd") + + if cmd == "ping": + response = {"ok": True} + elif cmd == "tool": + tool_name = request["tool_name"] + arguments = request.get("arguments", {}) + result = await session.call_tool(tool_name, arguments) + response = {"ok": True, "result": _normalize_tool_result(result)} + elif cmd == "shutdown": + response = {"ok": True, "result": "shutdown"} + writer.write((json.dumps(response) + "\n").encode("utf-8")) + await writer.drain() + return + else: + response = {"ok": False, "error": f"Unknown command: {cmd}"} + except Exception as e: + response = {"ok": False, "error": str(e)} + + writer.write((json.dumps(response) + "\n").encode("utf-8")) + await writer.drain() + try: + writer.close() + await writer.wait_closed() + except Exception: + pass + + server = await asyncio.start_unix_server(handle_client, path=str(socket_path)) + pid_path.write_text(str(os.getpid()), encoding="utf-8") + try: + async with server: + await server.serve_forever() + finally: + try: + socket_path.unlink() + except FileNotFoundError: + pass + try: + pid_path.unlink() + except FileNotFoundError: + pass + + +def _spawn_daemon_process() -> subprocess.Popen: + """Spawn the background daemon process detached from this CLI.""" + _ensure_daemon_runtime_dir() + cmd = [ + sys.executable, + "-c", + "import asyncio; from cli_anything.browser.utils.domshell_backend import _daemon_server_main; asyncio.run(_daemon_server_main())", + ] + return subprocess.Popen( + cmd, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + start_new_session=True, + env=os.environ.copy(), + ) + + +def _kill_pid(pid: int) -> None: + try: + os.kill(pid, signal.SIGTERM) + except ProcessLookupError: + return + except PermissionError: + return + + # Give it a moment to exit, then force-kill if needed. + for _ in range(20): + try: + os.kill(pid, 0) + except ProcessLookupError: + return + except PermissionError: + return + except OSError: + return + import time + time.sleep(0.1) + try: + os.kill(pid, signal.SIGKILL) + except Exception: + pass def _check_npx() -> bool: @@ -130,16 +334,27 @@ async def _call_tool( """ global _daemon_session, _daemon_read, _daemon_write + # 1) Preferred path: external daemon socket, if present. + if use_daemon and _daemon_is_socket_alive(): + response = await _daemon_request({ + "cmd": "tool", + "tool_name": tool_name, + "arguments": arguments, + }) + if not response.get("ok"): + raise RuntimeError(response.get("error", "Daemon tool call failed")) + return response.get("result") + + # 2) Legacy in-process daemon (kept for backwards compatibility inside the + # same Python process / REPL session). if use_daemon and _daemon_session is not None: - # Use persistent daemon connection try: result = await _daemon_session.call_tool(tool_name, arguments) - return result - except Exception as e: - # Daemon died, fall back to spawning new server + return _normalize_tool_result(result) + except Exception: await _stop_daemon() - # Spawn new MCP server process + # 3) One-shot spawn (default path). server_params = StdioServerParameters( command=DEFAULT_SERVER_CMD, args=_build_server_args() @@ -150,7 +365,7 @@ async def _call_tool( async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool(tool_name, arguments) - return result + return _normalize_tool_result(result) except Exception as e: raise RuntimeError( f"DOMShell MCP call failed: {e}\n" @@ -158,11 +373,9 @@ async def _call_tool( f"Chrome Web Store: https://chromewebstore.google.com/detail/domshell" ) from e -# NOTE: Known limitation - Daemon mode uses asyncio.run() per tool call (in sync wrappers). -# Each asyncio.run() creates a new event loop. Async IO objects created in one loop -# (like the daemon session) may have issues when accessed from subsequent calls that -# create new loops. This is a documented limitation for v1; future work should use -# a single long-lived event loop (e.g., background thread + run_coroutine_threadsafe). +# NOTE: Old v1 daemon mode used in-process state only, which could not survive +# across separate CLI invocations. The new daemon uses a background Unix-socket +# process; the in-process client is kept only as a fallback for the REPL. async def _start_daemon() -> bool: """Start persistent daemon mode. @@ -172,55 +385,87 @@ async def _start_daemon() -> bool: Raises: RuntimeError: If daemon fails to start """ - global _daemon_session, _daemon_read, _daemon_write, _daemon_client_context + global _daemon_session, _daemon_read, _daemon_write, _daemon_client_context, _daemon_process - if _daemon_session is not None: - return True # Already running + if _daemon_is_socket_alive(): + return True - server_params = StdioServerParameters( - command=DEFAULT_SERVER_CMD, - args=_build_server_args() - ) + # Clean up stale pid/socket from previous runs. + try: + stale_pid = _daemon_pid_path().read_text(encoding="utf-8").strip() + if stale_pid.isdigit(): + _kill_pid(int(stale_pid)) + except Exception: + pass + try: + _daemon_socket_path().unlink() + except FileNotFoundError: + pass + try: + _daemon_pid_path().unlink() + except FileNotFoundError: + pass try: - # Store the context manager so we can properly clean it up later - _daemon_client_context = stdio_client(server_params) - _daemon_read, _daemon_write = await _daemon_client_context.__aenter__() - _daemon_session = ClientSession(_daemon_read, _daemon_write) - await _daemon_session.__aenter__() - await _daemon_session.initialize() - return True + _daemon_process = _spawn_daemon_process() except Exception as e: - _daemon_session = None - _daemon_read = None - _daemon_write = None - _daemon_client_context = None - raise RuntimeError(f"Failed to start DOMShell daemon: {e}") from e + raise RuntimeError(f"Failed to spawn DOMShell daemon process: {e}") from e + + # Wait for the socket to become responsive. + for _ in range(100): + if _daemon_is_socket_alive(): + return True + await asyncio.sleep(0.1) + + raise RuntimeError("Failed to start DOMShell daemon: socket never became ready") async def _stop_daemon() -> None: """Stop persistent daemon mode.""" - global _daemon_session, _daemon_read, _daemon_write, _daemon_client_context - - if _daemon_session is None: - return + global _daemon_session, _daemon_read, _daemon_write, _daemon_client_context, _daemon_process + # Stop in-process session if present. + if _daemon_session is not None: + try: + await _daemon_session.__aexit__(None, None, None) + if _daemon_client_context: + await _daemon_client_context.__aexit__(None, None, None) + except Exception: + pass + finally: + _daemon_session = None + _daemon_read = None + _daemon_write = None + _daemon_client_context = None + + # Stop external daemon process if present. + pid = None try: - await _daemon_session.__aexit__(None, None, None) - if _daemon_client_context: - await _daemon_client_context.__aexit__(None, None, None) + pid_text = _daemon_pid_path().read_text(encoding="utf-8").strip() + if pid_text.isdigit(): + pid = int(pid_text) except Exception: - pass # Ignore cleanup errors - finally: - _daemon_session = None - _daemon_read = None - _daemon_write = None - _daemon_client_context = None + pid = None + + if pid is not None: + _kill_pid(pid) + + try: + _daemon_socket_path().unlink() + except FileNotFoundError: + pass + try: + _daemon_pid_path().unlink() + except FileNotFoundError: + pass + + _daemon_process = None def daemon_started() -> bool: """Check if daemon mode is active.""" - return _daemon_session is not None + return _daemon_is_socket_alive() or _daemon_session is not None + # ── Sync wrappers for each DOMShell tool ───────────────────────────── @@ -392,11 +637,23 @@ def type_text(path: str, text: str, use_daemon: bool = False) -> dict: Returns: Dict with action result """ + # Fast preflight: if the target path doesn't exist, return immediately and + # do not enter the focus/type flow (which can otherwise hang on bad paths). + preflight = cat(path, use_daemon=use_daemon) + preflight = _normalize_tool_result(preflight) + if _tool_result_has_error(preflight): + return preflight + async def _focus_and_type(): global _daemon_session + if use_daemon and _daemon_session is not None: - await _daemon_session.call_tool("domshell_focus", {"name": path}) - return await _daemon_session.call_tool("domshell_type", {"text": text}) + focus_result = await _daemon_session.call_tool("domshell_focus", {"name": path}) + focus_result = _normalize_tool_result(focus_result) + if _tool_result_has_error(focus_result): + return focus_result + type_result = await _daemon_session.call_tool("domshell_type", {"text": text}) + return _normalize_tool_result(type_result) server_params = StdioServerParameters( command=DEFAULT_SERVER_CMD, @@ -405,8 +662,12 @@ async def _focus_and_type(): async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() - await session.call_tool("domshell_focus", {"name": path}) - return await session.call_tool("domshell_type", {"text": text}) + focus_result = await session.call_tool("domshell_focus", {"name": path}) + focus_result = _normalize_tool_result(focus_result) + if _tool_result_has_error(focus_result): + return focus_result + type_result = await session.call_tool("domshell_type", {"text": text}) + return _normalize_tool_result(type_result) return asyncio.run(_focus_and_type()) diff --git a/browser/agent-harness/cli_anything/browser/utils/repl_skin.py b/browser/agent-harness/cli_anything/browser/utils/repl_skin.py index dab3acb5af..6f918f96a2 100644 --- a/browser/agent-harness/cli_anything/browser/utils/repl_skin.py +++ b/browser/agent-harness/cli_anything/browser/utils/repl_skin.py @@ -6,14 +6,14 @@ Usage: from cli_anything..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 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() """ @@ -47,8 +47,6 @@ "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 @@ -99,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", "browser"). + software: Software name (e.g., "gimp", "shotcut", "blender"). version: CLI version string. history_file: Path for persistent command history. Defaults to ~/.cli-anything-/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//utils/repl_skin.py (this file) + # cli_anything//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 @@ -144,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 · Browser - 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). @@ -189,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 = ""): @@ -221,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 @@ -234,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 @@ -301,7 +271,7 @@ def section(self, title: str): 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.""" @@ -395,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 ──────────────────────────────── @@ -495,8 +463,6 @@ def toolbar(): "\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 } diff --git a/browser/agent-harness/cli_anything/browser/utils/tool_result.py b/browser/agent-harness/cli_anything/browser/utils/tool_result.py new file mode 100644 index 0000000000..41f611d605 --- /dev/null +++ b/browser/agent-harness/cli_anything/browser/utils/tool_result.py @@ -0,0 +1,138 @@ +"""Shared helpers for DOMShell/MCP tool results. + +The browser harness often receives payloads where `isError=False` but the text +payload clearly says the operation failed. This module centralizes the best- +effort classification and message extraction so command handlers can make one +consistent decision instead of duplicating heuristics. +""" + +from __future__ import annotations + +from typing import Any, Iterable + +_FAILURE_TOKENS = ( + "no such", + "failed", + "could not", + "cannot", + "invalid", + "not found", + "denied", + "timed out", + "timeout", + "error occurred", + "an error occurred", + "error:", + "fatal", +) + +_ERROR_STATUS_VALUES = {"error", "failed", "failure"} + + +def normalize_tool_result(result: Any) -> Any: + """Convert tool result objects into JSON-friendly Python data.""" + if hasattr(result, "model_dump"): + return result.model_dump() + if hasattr(result, "dict"): + try: + return result.dict() + except Exception: + pass + if isinstance(result, (dict, list, str, int, float, bool)) or result is None: + return result + return str(result) + + +def _iter_text_payloads(result: Any) -> Iterable[str]: + data = normalize_tool_result(result) + if isinstance(data, str): + yield data + return + if not isinstance(data, dict): + return + + for key in ("error", "message", "detail", "stdout"): + value = data.get(key) + if isinstance(value, str) and value.strip(): + yield value + + content = data.get("content") + if isinstance(content, list): + for item in content: + if isinstance(item, dict): + text = item.get("text", "") + if isinstance(text, str) and text.strip(): + yield text + elif isinstance(item, str) and item.strip(): + yield item + + +def _looks_like_failure_text(text: str) -> bool: + lowered = text.lower().strip() + if not lowered: + return False + return any(token in lowered for token in _FAILURE_TOKENS) + + +def tool_result_has_error(result: Any) -> bool: + """Best-effort detection of a tool failure payload.""" + data = normalize_tool_result(result) + if isinstance(data, str): + return _looks_like_failure_text(data) + if not isinstance(data, dict): + return False + + if data.get("error") is not None: + return True + if data.get("isError") is True: + return True + status = data.get("status") + if isinstance(status, str) and status.lower().strip() in _ERROR_STATUS_VALUES: + return True + + for text in _iter_text_payloads(data): + if _looks_like_failure_text(text): + return True + + return False + + +def tool_result_error_text(result: Any, default: str = "") -> str: + """Extract a concise human-readable error text from a tool result.""" + data = normalize_tool_result(result) + if isinstance(data, str): + return data.strip() or default + if not isinstance(data, dict): + return default + + error_value = data.get("error") + if error_value is not None: + if isinstance(error_value, str) and error_value.strip(): + return error_value + return str(error_value) + + for text in _iter_text_payloads(data): + if _looks_like_failure_text(text): + return text + + return default + + +def tool_result_body_text(result: Any, default: str = "") -> str: + """Extract a concise success payload for human-readable output. + + Joins all textual payload fragments into a single string. If nothing useful + is present, returns `default`. + """ + data = normalize_tool_result(result) + if isinstance(data, str): + text = data.strip() + return text or default + if not isinstance(data, dict): + return default + + lines = [text.strip() for text in _iter_text_payloads(data) if text.strip()] + if lines: + return "\n".join(lines) + + return default diff --git a/chromadb/agent-harness/cli_anything/chromadb/utils/repl_skin.py b/chromadb/agent-harness/cli_anything/chromadb/utils/repl_skin.py index b356cb8357..6f918f96a2 100644 --- a/chromadb/agent-harness/cli_anything/chromadb/utils/repl_skin.py +++ b/chromadb/agent-harness/cli_anything/chromadb/utils/repl_skin.py @@ -6,14 +6,14 @@ Usage: from cli_anything..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 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() """ @@ -47,7 +47,6 @@ "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 @@ -98,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", "ollama"). + software: Software name (e.g., "gimp", "shotcut", "blender"). version: CLI version string. history_file: Path for persistent command history. Defaults to ~/.cli-anything-/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//utils/repl_skin.py (this file) + # cli_anything//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 @@ -143,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 · Ollama - 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). @@ -188,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 = ""): @@ -220,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 @@ -233,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 @@ -394,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 ──────────────────────────────── @@ -496,5 +465,4 @@ def toolbar(): "\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 } diff --git a/cli-anything-plugin/repl_skin.py b/cli-anything-plugin/repl_skin.py index c7312348a7..6f918f96a2 100644 --- a/cli-anything-plugin/repl_skin.py +++ b/cli-anything-plugin/repl_skin.py @@ -155,52 +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 = "" - - # 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 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). @@ -210,28 +176,15 @@ def prompt(self, project_name: str = "", modified: bool = False, Returns: Formatted prompt string. """ - parts = [] + parts = [self.software] - # 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(f"[{ctx}{mod}]") - parts.append(self._c(_GRAY, " ❯ ")) - - return "".join(parts) + parts.append(">") + return " ".join(parts) + " " def prompt_tokens(self, project_name: str = "", modified: bool = False, context: str = ""): @@ -242,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 @@ -255,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 @@ -416,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 ──────────────────────────────── diff --git a/cloudanalyzer/agent-harness/cli_anything/cloudanalyzer/utils/repl_skin.py b/cloudanalyzer/agent-harness/cli_anything/cloudanalyzer/utils/repl_skin.py index c7312348a7..6f918f96a2 100644 --- a/cloudanalyzer/agent-harness/cli_anything/cloudanalyzer/utils/repl_skin.py +++ b/cloudanalyzer/agent-harness/cli_anything/cloudanalyzer/utils/repl_skin.py @@ -155,52 +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 = "" - - # 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 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). @@ -210,28 +176,15 @@ def prompt(self, project_name: str = "", modified: bool = False, Returns: Formatted prompt string. """ - parts = [] + parts = [self.software] - # 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(f"[{ctx}{mod}]") - parts.append(self._c(_GRAY, " ❯ ")) - - return "".join(parts) + parts.append(">") + return " ".join(parts) + " " def prompt_tokens(self, project_name: str = "", modified: bool = False, context: str = ""): @@ -242,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 @@ -255,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 @@ -416,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 ──────────────────────────────── diff --git a/cloudcompare/agent-harness/cli_anything/cloudcompare/utils/repl_skin.py b/cloudcompare/agent-harness/cli_anything/cloudcompare/utils/repl_skin.py index c7312348a7..6f918f96a2 100644 --- a/cloudcompare/agent-harness/cli_anything/cloudcompare/utils/repl_skin.py +++ b/cloudcompare/agent-harness/cli_anything/cloudcompare/utils/repl_skin.py @@ -155,52 +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 = "" - - # 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 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). @@ -210,28 +176,15 @@ def prompt(self, project_name: str = "", modified: bool = False, Returns: Formatted prompt string. """ - parts = [] + parts = [self.software] - # 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(f"[{ctx}{mod}]") - parts.append(self._c(_GRAY, " ❯ ")) - - return "".join(parts) + parts.append(">") + return " ".join(parts) + " " def prompt_tokens(self, project_name: str = "", modified: bool = False, context: str = ""): @@ -242,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 @@ -255,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 @@ -416,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 ──────────────────────────────── diff --git a/dify-workflow/agent-harness/cli_anything/dify_workflow/utils/repl_skin.py b/dify-workflow/agent-harness/cli_anything/dify_workflow/utils/repl_skin.py index c7312348a7..6f918f96a2 100644 --- a/dify-workflow/agent-harness/cli_anything/dify_workflow/utils/repl_skin.py +++ b/dify-workflow/agent-harness/cli_anything/dify_workflow/utils/repl_skin.py @@ -155,52 +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 = "" - - # 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 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). @@ -210,28 +176,15 @@ def prompt(self, project_name: str = "", modified: bool = False, Returns: Formatted prompt string. """ - parts = [] + parts = [self.software] - # 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(f"[{ctx}{mod}]") - parts.append(self._c(_GRAY, " ❯ ")) - - return "".join(parts) + parts.append(">") + return " ".join(parts) + " " def prompt_tokens(self, project_name: str = "", modified: bool = False, context: str = ""): @@ -242,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 @@ -255,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 @@ -416,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 ──────────────────────────────── diff --git a/drawio/agent-harness/cli_anything/drawio/utils/repl_skin.py b/drawio/agent-harness/cli_anything/drawio/utils/repl_skin.py index 93e2f352fd..6f918f96a2 100644 --- a/drawio/agent-harness/cli_anything/drawio/utils/repl_skin.py +++ b/drawio/agent-harness/cli_anything/drawio/utils/repl_skin.py @@ -7,7 +7,7 @@ from cli_anything..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") @@ -47,7 +47,6 @@ "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 @@ -98,7 +97,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,10 +105,23 @@ def __init__(self, software: str, version: str = "1.0.0", version: CLI version string. history_file: Path for persistent command history. Defaults to ~/.cli-anything-/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//utils/repl_skin.py (this file) + # cli_anything//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 @@ -143,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). @@ -188,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 = ""): @@ -220,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 @@ -233,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 @@ -394,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 ──────────────────────────────── @@ -495,6 +464,5 @@ def toolbar(): "\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 } diff --git a/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/utils/repl_skin.py b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/utils/repl_skin.py index 47260bebd0..6f918f96a2 100644 --- a/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/utils/repl_skin.py +++ b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/utils/repl_skin.py @@ -7,7 +7,7 @@ from cli_anything..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") @@ -97,7 +97,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,10 +105,23 @@ def __init__(self, software: str, version: str = "1.0.0", version: CLI version string. history_file: Path for persistent command history. Defaults to ~/.cli-anything-/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//utils/repl_skin.py (this file) + # cli_anything//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 @@ -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). @@ -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 = ""): @@ -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 @@ -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 @@ -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 ──────────────────────────────── diff --git a/exa/agent-harness/cli_anything/exa/utils/repl_skin.py b/exa/agent-harness/cli_anything/exa/utils/repl_skin.py index c7312348a7..6f918f96a2 100644 --- a/exa/agent-harness/cli_anything/exa/utils/repl_skin.py +++ b/exa/agent-harness/cli_anything/exa/utils/repl_skin.py @@ -155,52 +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 = "" - - # 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 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). @@ -210,28 +176,15 @@ def prompt(self, project_name: str = "", modified: bool = False, Returns: Formatted prompt string. """ - parts = [] + parts = [self.software] - # 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(f"[{ctx}{mod}]") - parts.append(self._c(_GRAY, " ❯ ")) - - return "".join(parts) + parts.append(">") + return " ".join(parts) + " " def prompt_tokens(self, project_name: str = "", modified: bool = False, context: str = ""): @@ -242,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 @@ -255,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 @@ -416,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 ──────────────────────────────── diff --git a/freecad/agent-harness/cli_anything/freecad/utils/repl_skin.py b/freecad/agent-harness/cli_anything/freecad/utils/repl_skin.py index 962c97bb8d..6f918f96a2 100644 --- a/freecad/agent-harness/cli_anything/freecad/utils/repl_skin.py +++ b/freecad/agent-harness/cli_anything/freecad/utils/repl_skin.py @@ -47,7 +47,6 @@ "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 @@ -156,52 +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 = "" - - # 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 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). @@ -211,28 +176,15 @@ def prompt(self, project_name: str = "", modified: bool = False, Returns: Formatted prompt string. """ - parts = [] + parts = [self.software] - # 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(f"[{ctx}{mod}]") - parts.append(self._c(_GRAY, " ❯ ")) - - return "".join(parts) + parts.append(">") + return " ".join(parts) + " " def prompt_tokens(self, project_name: str = "", modified: bool = False, context: str = ""): @@ -243,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 @@ -256,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 @@ -417,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 ──────────────────────────────── @@ -518,6 +464,5 @@ def toolbar(): "\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 } diff --git a/gimp/agent-harness/cli_anything/gimp/utils/repl_skin.py b/gimp/agent-harness/cli_anything/gimp/utils/repl_skin.py index 47260bebd0..6f918f96a2 100644 --- a/gimp/agent-harness/cli_anything/gimp/utils/repl_skin.py +++ b/gimp/agent-harness/cli_anything/gimp/utils/repl_skin.py @@ -7,7 +7,7 @@ from cli_anything..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") @@ -97,7 +97,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,10 +105,23 @@ def __init__(self, software: str, version: str = "1.0.0", version: CLI version string. history_file: Path for persistent command history. Defaults to ~/.cli-anything-/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//utils/repl_skin.py (this file) + # cli_anything//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 @@ -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). @@ -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 = ""): @@ -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 @@ -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 @@ -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 ──────────────────────────────── diff --git a/godot/agent-harness/cli_anything/godot/utils/repl_skin.py b/godot/agent-harness/cli_anything/godot/utils/repl_skin.py index c7312348a7..6f918f96a2 100644 --- a/godot/agent-harness/cli_anything/godot/utils/repl_skin.py +++ b/godot/agent-harness/cli_anything/godot/utils/repl_skin.py @@ -155,52 +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 = "" - - # 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 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). @@ -210,28 +176,15 @@ def prompt(self, project_name: str = "", modified: bool = False, Returns: Formatted prompt string. """ - parts = [] + parts = [self.software] - # 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(f"[{ctx}{mod}]") - parts.append(self._c(_GRAY, " ❯ ")) - - return "".join(parts) + parts.append(">") + return " ".join(parts) + " " def prompt_tokens(self, project_name: str = "", modified: bool = False, context: str = ""): @@ -242,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 @@ -255,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 @@ -416,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 ──────────────────────────────── diff --git a/inkscape/agent-harness/cli_anything/inkscape/utils/repl_skin.py b/inkscape/agent-harness/cli_anything/inkscape/utils/repl_skin.py index 47260bebd0..6f918f96a2 100644 --- a/inkscape/agent-harness/cli_anything/inkscape/utils/repl_skin.py +++ b/inkscape/agent-harness/cli_anything/inkscape/utils/repl_skin.py @@ -7,7 +7,7 @@ from cli_anything..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") @@ -97,7 +97,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,10 +105,23 @@ def __init__(self, software: str, version: str = "1.0.0", version: CLI version string. history_file: Path for persistent command history. Defaults to ~/.cli-anything-/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//utils/repl_skin.py (this file) + # cli_anything//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 @@ -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). @@ -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 = ""): @@ -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 @@ -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 @@ -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 ──────────────────────────────── diff --git a/intelwatch/agent-harness/cli_anything/intelwatch/utils/repl_skin.py b/intelwatch/agent-harness/cli_anything/intelwatch/utils/repl_skin.py index 47260bebd0..6f918f96a2 100644 --- a/intelwatch/agent-harness/cli_anything/intelwatch/utils/repl_skin.py +++ b/intelwatch/agent-harness/cli_anything/intelwatch/utils/repl_skin.py @@ -7,7 +7,7 @@ from cli_anything..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") @@ -97,7 +97,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,10 +105,23 @@ def __init__(self, software: str, version: str = "1.0.0", version: CLI version string. history_file: Path for persistent command history. Defaults to ~/.cli-anything-/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//utils/repl_skin.py (this file) + # cli_anything//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 @@ -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). @@ -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 = ""): @@ -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 @@ -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 @@ -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 ──────────────────────────────── diff --git a/iterm2/agent-harness/cli_anything/iterm2_ctl/utils/repl_skin.py b/iterm2/agent-harness/cli_anything/iterm2_ctl/utils/repl_skin.py index c7312348a7..6f918f96a2 100644 --- a/iterm2/agent-harness/cli_anything/iterm2_ctl/utils/repl_skin.py +++ b/iterm2/agent-harness/cli_anything/iterm2_ctl/utils/repl_skin.py @@ -155,52 +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 = "" - - # 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 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). @@ -210,28 +176,15 @@ def prompt(self, project_name: str = "", modified: bool = False, Returns: Formatted prompt string. """ - parts = [] + parts = [self.software] - # 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(f"[{ctx}{mod}]") - parts.append(self._c(_GRAY, " ❯ ")) - - return "".join(parts) + parts.append(">") + return " ".join(parts) + " " def prompt_tokens(self, project_name: str = "", modified: bool = False, context: str = ""): @@ -242,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 @@ -255,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 @@ -416,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 ──────────────────────────────── diff --git a/kdenlive/agent-harness/cli_anything/kdenlive/utils/repl_skin.py b/kdenlive/agent-harness/cli_anything/kdenlive/utils/repl_skin.py index 47260bebd0..6f918f96a2 100644 --- a/kdenlive/agent-harness/cli_anything/kdenlive/utils/repl_skin.py +++ b/kdenlive/agent-harness/cli_anything/kdenlive/utils/repl_skin.py @@ -7,7 +7,7 @@ from cli_anything..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") @@ -97,7 +97,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,10 +105,23 @@ def __init__(self, software: str, version: str = "1.0.0", version: CLI version string. history_file: Path for persistent command history. Defaults to ~/.cli-anything-/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//utils/repl_skin.py (this file) + # cli_anything//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 @@ -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). @@ -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 = ""): @@ -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 @@ -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 @@ -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 ──────────────────────────────── diff --git a/krita/agent-harness/cli_anything/krita/utils/repl_skin.py b/krita/agent-harness/cli_anything/krita/utils/repl_skin.py index c3fd1db2fc..6f918f96a2 100644 --- a/krita/agent-harness/cli_anything/krita/utils/repl_skin.py +++ b/krita/agent-harness/cli_anything/krita/utils/repl_skin.py @@ -47,7 +47,6 @@ "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 @@ -156,52 +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 = "" - - # 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 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). @@ -211,28 +176,15 @@ def prompt(self, project_name: str = "", modified: bool = False, Returns: Formatted prompt string. """ - parts = [] + parts = [self.software] - # 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(f"[{ctx}{mod}]") - parts.append(self._c(_GRAY, " ❯ ")) - - return "".join(parts) + parts.append(">") + return " ".join(parts) + " " def prompt_tokens(self, project_name: str = "", modified: bool = False, context: str = ""): @@ -243,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 @@ -256,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 @@ -417,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 ──────────────────────────────── @@ -516,7 +462,6 @@ def toolbar(): "\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 diff --git a/libreoffice/agent-harness/cli_anything/libreoffice/utils/repl_skin.py b/libreoffice/agent-harness/cli_anything/libreoffice/utils/repl_skin.py index 47260bebd0..6f918f96a2 100644 --- a/libreoffice/agent-harness/cli_anything/libreoffice/utils/repl_skin.py +++ b/libreoffice/agent-harness/cli_anything/libreoffice/utils/repl_skin.py @@ -7,7 +7,7 @@ from cli_anything..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") @@ -97,7 +97,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,10 +105,23 @@ def __init__(self, software: str, version: str = "1.0.0", version: CLI version string. history_file: Path for persistent command history. Defaults to ~/.cli-anything-/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//utils/repl_skin.py (this file) + # cli_anything//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 @@ -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). @@ -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 = ""): @@ -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 @@ -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 @@ -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 ──────────────────────────────── diff --git a/mermaid/agent-harness/cli_anything/mermaid/utils/repl_skin.py b/mermaid/agent-harness/cli_anything/mermaid/utils/repl_skin.py index 5de4ac4204..6f918f96a2 100644 --- a/mermaid/agent-harness/cli_anything/mermaid/utils/repl_skin.py +++ b/mermaid/agent-harness/cli_anything/mermaid/utils/repl_skin.py @@ -1,44 +1,468 @@ -"""Minimal REPL skin compatible with CLI-Anything REPL usage.""" +"""cli-anything REPL Skin — Unified terminal interface for all CLI harnesses. -from __future__ import annotations +Copy this file into your CLI package at: + cli_anything//utils/repl_skin.py -from prompt_toolkit import PromptSession -from prompt_toolkit.auto_suggest import AutoSuggestFromHistory -from prompt_toolkit.history import InMemoryHistory +Usage: + from cli_anything..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: - def __init__(self, software: str, version: str = "1.0.0"): - self.software = software + """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-/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 - def print_banner(self) -> None: - print(f"cli-anything-{self.software} v{self.version}") - print("Type help for commands, quit to exit") + # Auto-detect skill path from package layout: + # cli_anything//utils/repl_skin.py (this file) + # cli_anything//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 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 plain prompt string for interactive use. + + 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 = [self.software] + + if project_name or context: + ctx = context or project_name + mod = "*" if modified else "" + parts.append(f"[{ctx}{mod}]") + + parts.append(">") + 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. + """ + tokens = [("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 compact help listing. + + Args: + commands: Dict of command -> description pairs. + """ + print("Commands:") + max_cmd = max(len(c) for c in commands) if commands else 0 + for cmd, desc in commands.items(): + print(f" {cmd:<{max_cmd}} {desc}") + print() + + # ── Goodbye ─────────────────────────────────────────────────────── + + def print_goodbye(self): + """Print a compact goodbye message.""" + print("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. - def create_prompt_session(self) -> PromptSession: - return PromptSession(history=InMemoryHistory(), auto_suggest=AutoSuggestFromHistory()) + 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() - def get_input(self, session: PromptSession, project_name: str = "", modified: bool = False) -> str: - suffix = "*" if modified else "" - ctx = f"[{project_name}{suffix}]" if project_name else "" - return session.prompt(f"{self.software}{ctx}> ") + # ── Toolbar builder ─────────────────────────────────────────────── - def help(self, commands: dict[str, str]) -> None: - for command, desc in commands.items(): - print(f"{command}: {desc}") + def bottom_toolbar(self, items: dict[str, str]): + """Create a bottom toolbar callback for prompt_toolkit. - def success(self, message: str) -> None: - print(f"OK {message}") + Args: + items: Dict of label -> value pairs to show in toolbar. - def error(self, message: str) -> None: - print(f"ERROR {message}") + 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 - def warning(self, message: str) -> None: - print(f"WARN {message}") - def info(self, message: str) -> None: - print(f"INFO {message}") +# ── ANSI 256-color to hex mapping (for prompt_toolkit styles) ───────── - def print_goodbye(self) -> None: - print("Goodbye!") +_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 +} diff --git a/mubu/agent-harness/cli_anything/mubu/utils/repl_skin.py b/mubu/agent-harness/cli_anything/mubu/utils/repl_skin.py index c7312348a7..6f918f96a2 100644 --- a/mubu/agent-harness/cli_anything/mubu/utils/repl_skin.py +++ b/mubu/agent-harness/cli_anything/mubu/utils/repl_skin.py @@ -155,52 +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 = "" - - # 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 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). @@ -210,28 +176,15 @@ def prompt(self, project_name: str = "", modified: bool = False, Returns: Formatted prompt string. """ - parts = [] + parts = [self.software] - # 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(f"[{ctx}{mod}]") - parts.append(self._c(_GRAY, " ❯ ")) - - return "".join(parts) + parts.append(">") + return " ".join(parts) + " " def prompt_tokens(self, project_name: str = "", modified: bool = False, context: str = ""): @@ -242,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 @@ -255,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 @@ -416,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 ──────────────────────────────── diff --git a/musescore/agent-harness/cli_anything/musescore/utils/repl_skin.py b/musescore/agent-harness/cli_anything/musescore/utils/repl_skin.py index 47260bebd0..6f918f96a2 100644 --- a/musescore/agent-harness/cli_anything/musescore/utils/repl_skin.py +++ b/musescore/agent-harness/cli_anything/musescore/utils/repl_skin.py @@ -7,7 +7,7 @@ from cli_anything..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") @@ -97,7 +97,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,10 +105,23 @@ def __init__(self, software: str, version: str = "1.0.0", version: CLI version string. history_file: Path for persistent command history. Defaults to ~/.cli-anything-/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//utils/repl_skin.py (this file) + # cli_anything//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 @@ -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). @@ -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 = ""): @@ -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 @@ -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 @@ -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 ──────────────────────────────── diff --git a/n8n/agent-harness/cli_anything/n8n/utils/repl_skin.py b/n8n/agent-harness/cli_anything/n8n/utils/repl_skin.py index be7e0751ef..6f918f96a2 100644 --- a/n8n/agent-harness/cli_anything/n8n/utils/repl_skin.py +++ b/n8n/agent-harness/cli_anything/n8n/utils/repl_skin.py @@ -6,25 +6,20 @@ Usage: from cli_anything..utils.repl_skin import ReplSkin - skin = ReplSkin("n8n", version="2.4.7") + 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_workflow", modified=True) - skin.success("Workflow activated") - skin.error("Connection failed") + 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 workflows...") - skin.status("Status", "Connected") + skin.info("Processing 24 clips...") + skin.status("Track 1", "3 clips, 00:02:30") skin.table(headers, rows) skin.print_goodbye() """ -import json import os -import shutil import sys -from typing import Any - -import click # ── ANSI color codes (no external deps for core styling) ────────────── @@ -52,7 +47,6 @@ "obs_studio": "\033[38;5;55m", # purple "kdenlive": "\033[38;5;69m", # slate blue "shotcut": "\033[38;5;35m", # teal green - "n8n": "\033[38;5;203m", # n8n coral/red (#EA4B71) } _DEFAULT_ACCENT = "\033[38;5;75m" # default sky blue @@ -161,52 +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 · N8N - 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 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). @@ -216,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("> ") - - # Software name - parts.append(self._c(self.accent + _BOLD, self.software)) + parts = [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(f"[{ctx}{mod}]") - parts.append(self._c(_GRAY, " ❯ ")) - - return "".join(parts) + parts.append(">") + return " ".join(parts) + " " def prompt_tokens(self, project_name: str = "", modified: bool = False, context: str = ""): @@ -248,10 +195,7 @@ def prompt_tokens(self, project_name: str = "", modified: bool = False, Returns: list of (style, text) tuples for prompt_toolkit. """ - 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 @@ -260,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 @@ -362,7 +306,7 @@ def progress(self, current: int, total: int, label: str = ""): 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 = "\u2588" * filled + "\u2591" * (bar_width - filled) + 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)}" @@ -405,7 +349,9 @@ def pad(text: str, width: int) -> str: print(header_line) # Separator - print(self._c(_DARK_GRAY, f" {'───'.join([_H_LINE * w for w in col_widths])}")) + 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: @@ -419,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 ──────────────────────────────── @@ -450,6 +394,7 @@ def create_prompt_session(self): 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() @@ -518,141 +463,6 @@ def toolbar(): "\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;203m": "#ff5f5f", # n8n coral "\033[38;5;208m": "#ff8700", # blender deep orange "\033[38;5;214m": "#ffaf00", # gimp warm orange } - - -# ── Module-level convenience wrappers ───────────────────────────────── -# These delegate to a lazily-initialized ReplSkin singleton so that -# n8n_cli.py can do `from ...repl_skin import success, error, warn` -# while still routing through the standard ReplSkin class. - -_skin: ReplSkin | None = None - - -def _get_skin() -> ReplSkin: - global _skin - if _skin is None: - # Import VERSION lazily to avoid circular imports - try: - from cli_anything.n8n.n8n_cli import VERSION - except ImportError: - VERSION = "0.0.0" - _skin = ReplSkin("n8n", version=VERSION) - return _skin - - -def print_banner() -> None: - """Print the cli-anything branded banner.""" - _get_skin().print_banner() - - -def success(msg: str) -> None: - """Print a success message.""" - _get_skin().success(msg) - - -def error(msg: str) -> None: - """Print an error message.""" - _get_skin().error(msg) - - -def warn(msg: str) -> None: - """Print a warning message.""" - _get_skin().warning(msg) - - -# ── n8n-specific output helpers ─────────────────────────────────────── -# These handle --json flag and n8n API response formatting. -# Not part of the standard ReplSkin because they depend on click and -# n8n API response structure (data/nextCursor pagination). - -def output(data: Any, as_json: bool) -> None: - """Print data as JSON or human-readable.""" - if as_json: - click.echo(json.dumps(data, indent=2, default=str)) - return - - if isinstance(data, dict): - if "data" in data and isinstance(data["data"], list): - _print_table(data["data"]) - if "nextCursor" in data: - click.secho(f"\n Next cursor: {data['nextCursor']}", fg="bright_black") - else: - _print_dict(data) - elif isinstance(data, list): - if data and isinstance(data[0], dict): - _print_table(data) - else: - click.echo(json.dumps(data, indent=2, default=str)) - else: - click.echo(str(data)) - - -def _print_dict(d: dict[str, Any], indent: int = 0) -> None: - prefix = " " * indent - for k, v in d.items(): - if isinstance(v, dict): - click.secho(f"{prefix}{k}:", fg="cyan") - _print_dict(v, indent + 1) - elif isinstance(v, list) and v and isinstance(v[0], dict): - click.secho(f"{prefix}{k}:", fg="cyan") - _print_table(v) - else: - click.echo(f"{prefix}{click.style(str(k), fg='cyan')}: {v}") - - -def _print_table(rows: list[dict[str, Any]]) -> None: - if not rows: - click.secho(" (empty)", fg="bright_black") - return - - term_width = shutil.get_terminal_size().columns - keys = list(rows[0].keys()) - - # Filter out overly complex nested fields for table view - simple_keys = [k for k in keys if not isinstance(rows[0].get(k), (dict, list))] - if not simple_keys: - simple_keys = keys[:5] - - col_widths = {k: len(str(k)) for k in simple_keys} - for row in rows: - for k in simple_keys: - val = str(row.get(k, "")) - col_widths[k] = min(max(col_widths[k], len(val)), 40) - - # Truncate columns if they exceed terminal width - total = sum(col_widths.values()) + (len(simple_keys) - 1) * 3 - if total > term_width: - max_col = max(10, term_width // len(simple_keys) - 3) - col_widths = {k: min(v, max_col) for k, v in col_widths.items()} - - header = " | ".join( - click.style(k.ljust(col_widths[k])[:col_widths[k]], fg="cyan") - for k in simple_keys - ) - click.echo(header) - click.echo("-+-".join("-" * col_widths[k] for k in simple_keys)) - - # Color rules for specific columns - color_rules = { - "status": {"success": "green", "error": "red", "running": "bright_yellow", "waiting": "cyan"}, - "active": {"True": "green", "False": "bright_black"}, - } - - for row in rows: - vals = [] - for k in simple_keys: - v = str(row.get(k, "")) - w = col_widths[k] - if len(v) > w and w > 3: - cell = v[: w - 1] + "\u2026" - else: - cell = v.ljust(w)[:w] - # Apply color if column has a rule - if k in color_rules and v in color_rules[k]: - cell = click.style(cell, fg=color_rules[k][v]) - vals.append(cell) - click.echo(" | ".join(vals)) diff --git a/novita/agent-harness/cli_anything/novita/utils/repl_skin.py b/novita/agent-harness/cli_anything/novita/utils/repl_skin.py index 68d5a179a7..6f918f96a2 100644 --- a/novita/agent-harness/cli_anything/novita/utils/repl_skin.py +++ b/novita/agent-harness/cli_anything/novita/utils/repl_skin.py @@ -7,7 +7,7 @@ from cli_anything..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") @@ -30,7 +30,7 @@ _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 +39,16 @@ # 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" @@ -83,7 +81,6 @@ def _strip_ansi(text: str) -> str: """Remove ANSI escape codes for length calculation.""" import re - return re.sub(r"\033\[[^m]*m", "", text) @@ -99,9 +96,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 +105,28 @@ def __init__( version: CLI version string. history_file: Path for persistent command history. Defaults to ~/.cli-anything-/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//utils/repl_skin.py (this file) + # cli_anything//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") @@ -147,43 +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 · Novita - 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(). + def prompt(self, project_name: str = "", modified: bool = False, + context: str = "") -> str: + """Build a plain prompt string for interactive use. Args: project_name: Current project name (empty if none open). @@ -193,32 +176,18 @@ def 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)) + parts = [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(f"[{ctx}{mod}]") - parts.append(self._c(_GRAY, " ❯ ")) + parts.append(">") + return " ".join(parts) + " " - 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. @@ -226,11 +195,7 @@ def prompt_tokens( 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 @@ -239,7 +204,7 @@ def prompt_tokens( tokens.append(("class:context", f"{ctx}{mod}")) tokens.append(("class:bracket", "]")) - tokens.append(("class:arrow", " ❯ ")) + tokens.append(("class:arrow", " > ")) return tokens @@ -256,25 +221,23 @@ def get_prompt_style(self): 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 +314,8 @@ def progress(self, current: int, total: int, label: str = ""): # ── 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 +341,8 @@ def pad(text: str, width: int) -> str: # 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 +350,7 @@ def pad(text: str, width: int) -> str: # 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 @@ -402,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 ──────────────────────────────── @@ -447,13 +408,8 @@ def create_prompt_session(self): 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 +423,6 @@ def get_input( """ 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 +440,8 @@ def bottom_toolbar(self, items: dict[str, str]): 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 +449,20 @@ def toolbar(): 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 } diff --git a/obs-studio/agent-harness/cli_anything/obs_studio/utils/repl_skin.py b/obs-studio/agent-harness/cli_anything/obs_studio/utils/repl_skin.py index 47260bebd0..6f918f96a2 100644 --- a/obs-studio/agent-harness/cli_anything/obs_studio/utils/repl_skin.py +++ b/obs-studio/agent-harness/cli_anything/obs_studio/utils/repl_skin.py @@ -7,7 +7,7 @@ from cli_anything..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") @@ -97,7 +97,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,10 +105,23 @@ def __init__(self, software: str, version: str = "1.0.0", version: CLI version string. history_file: Path for persistent command history. Defaults to ~/.cli-anything-/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//utils/repl_skin.py (this file) + # cli_anything//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 @@ -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). @@ -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 = ""): @@ -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 @@ -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 @@ -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 ──────────────────────────────── diff --git a/obsidian/agent-harness/cli_anything/obsidian/utils/repl_skin.py b/obsidian/agent-harness/cli_anything/obsidian/utils/repl_skin.py index b356cb8357..6f918f96a2 100644 --- a/obsidian/agent-harness/cli_anything/obsidian/utils/repl_skin.py +++ b/obsidian/agent-harness/cli_anything/obsidian/utils/repl_skin.py @@ -6,14 +6,14 @@ Usage: from cli_anything..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 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() """ @@ -47,7 +47,6 @@ "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 @@ -98,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", "ollama"). + software: Software name (e.g., "gimp", "shotcut", "blender"). version: CLI version string. history_file: Path for persistent command history. Defaults to ~/.cli-anything-/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//utils/repl_skin.py (this file) + # cli_anything//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 @@ -143,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 · Ollama - 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). @@ -188,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 = ""): @@ -220,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 @@ -233,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 @@ -394,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 ──────────────────────────────── @@ -496,5 +465,4 @@ def toolbar(): "\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 } diff --git a/ollama/agent-harness/cli_anything/ollama/utils/repl_skin.py b/ollama/agent-harness/cli_anything/ollama/utils/repl_skin.py index b356cb8357..6f918f96a2 100644 --- a/ollama/agent-harness/cli_anything/ollama/utils/repl_skin.py +++ b/ollama/agent-harness/cli_anything/ollama/utils/repl_skin.py @@ -6,14 +6,14 @@ Usage: from cli_anything..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 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() """ @@ -47,7 +47,6 @@ "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 @@ -98,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", "ollama"). + software: Software name (e.g., "gimp", "shotcut", "blender"). version: CLI version string. history_file: Path for persistent command history. Defaults to ~/.cli-anything-/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//utils/repl_skin.py (this file) + # cli_anything//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 @@ -143,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 · Ollama - 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). @@ -188,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 = ""): @@ -220,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 @@ -233,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 @@ -394,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 ──────────────────────────────── @@ -496,5 +465,4 @@ def toolbar(): "\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 } diff --git a/openscreen/agent-harness/cli_anything/openscreen/utils/repl_skin.py b/openscreen/agent-harness/cli_anything/openscreen/utils/repl_skin.py index 2feb59d1b7..6f918f96a2 100644 --- a/openscreen/agent-harness/cli_anything/openscreen/utils/repl_skin.py +++ b/openscreen/agent-harness/cli_anything/openscreen/utils/repl_skin.py @@ -7,7 +7,7 @@ from cli_anything..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") @@ -97,7 +97,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,10 +105,23 @@ def __init__(self, software: str, version: str = "1.0.0", version: CLI version string. history_file: Path for persistent command history. Defaults to ~/.cli-anything-/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//utils/repl_skin.py (this file) + # cli_anything//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 @@ -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). @@ -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("> ") - - # Software name - parts.append(self._c(self.accent + _BOLD, self.software)) + parts = [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(f"[{ctx}{mod}]") - parts.append(self._c(_GRAY, " ❯ ")) - - return "".join(parts) + parts.append(">") + return " ".join(parts) + " " def prompt_tokens(self, project_name: str = "", modified: bool = False, context: str = ""): @@ -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 @@ -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 @@ -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 ──────────────────────────────── @@ -459,26 +429,6 @@ def get_input(self, pt_session, project_name: str = "", 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]): diff --git a/pm2/agent-harness/cli_anything/pm2/utils/repl_skin.py b/pm2/agent-harness/cli_anything/pm2/utils/repl_skin.py index b356cb8357..6f918f96a2 100644 --- a/pm2/agent-harness/cli_anything/pm2/utils/repl_skin.py +++ b/pm2/agent-harness/cli_anything/pm2/utils/repl_skin.py @@ -6,14 +6,14 @@ Usage: from cli_anything..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 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() """ @@ -47,7 +47,6 @@ "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 @@ -98,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", "ollama"). + software: Software name (e.g., "gimp", "shotcut", "blender"). version: CLI version string. history_file: Path for persistent command history. Defaults to ~/.cli-anything-/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//utils/repl_skin.py (this file) + # cli_anything//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 @@ -143,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 · Ollama - 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). @@ -188,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 = ""): @@ -220,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 @@ -233,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 @@ -394,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 ──────────────────────────────── @@ -496,5 +465,4 @@ def toolbar(): "\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 } diff --git a/renderdoc/agent-harness/cli_anything/renderdoc/utils/repl_skin.py b/renderdoc/agent-harness/cli_anything/renderdoc/utils/repl_skin.py index c7312348a7..6f918f96a2 100644 --- a/renderdoc/agent-harness/cli_anything/renderdoc/utils/repl_skin.py +++ b/renderdoc/agent-harness/cli_anything/renderdoc/utils/repl_skin.py @@ -155,52 +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 = "" - - # 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 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). @@ -210,28 +176,15 @@ def prompt(self, project_name: str = "", modified: bool = False, Returns: Formatted prompt string. """ - parts = [] + parts = [self.software] - # 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(f"[{ctx}{mod}]") - parts.append(self._c(_GRAY, " ❯ ")) - - return "".join(parts) + parts.append(">") + return " ".join(parts) + " " def prompt_tokens(self, project_name: str = "", modified: bool = False, context: str = ""): @@ -242,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 @@ -255,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 @@ -416,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 ──────────────────────────────── diff --git a/rms/agent-harness/cli_anything/rms/utils/repl_skin.py b/rms/agent-harness/cli_anything/rms/utils/repl_skin.py index 75ac09b1bb..6f918f96a2 100644 --- a/rms/agent-harness/cli_anything/rms/utils/repl_skin.py +++ b/rms/agent-harness/cli_anything/rms/utils/repl_skin.py @@ -47,9 +47,6 @@ "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 @@ -158,52 +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 = "" - - # 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 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). @@ -213,28 +176,15 @@ def prompt(self, project_name: str = "", modified: bool = False, Returns: Formatted prompt string. """ - parts = [] + parts = [self.software] - # 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(f"[{ctx}{mod}]") - parts.append(self._c(_GRAY, " ❯ ")) - - return "".join(parts) + parts.append(">") + return " ".join(parts) + " " def prompt_tokens(self, project_name: str = "", modified: bool = False, context: str = ""): @@ -245,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 @@ -258,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 @@ -419,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 ──────────────────────────────── diff --git a/seaclip/agent-harness/cli_anything/seaclip/utils/repl_skin.py b/seaclip/agent-harness/cli_anything/seaclip/utils/repl_skin.py index b356cb8357..6f918f96a2 100644 --- a/seaclip/agent-harness/cli_anything/seaclip/utils/repl_skin.py +++ b/seaclip/agent-harness/cli_anything/seaclip/utils/repl_skin.py @@ -6,14 +6,14 @@ Usage: from cli_anything..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 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() """ @@ -47,7 +47,6 @@ "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 @@ -98,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", "ollama"). + software: Software name (e.g., "gimp", "shotcut", "blender"). version: CLI version string. history_file: Path for persistent command history. Defaults to ~/.cli-anything-/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//utils/repl_skin.py (this file) + # cli_anything//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 @@ -143,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 · Ollama - 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). @@ -188,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 = ""): @@ -220,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 @@ -233,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 @@ -394,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 ──────────────────────────────── @@ -496,5 +465,4 @@ def toolbar(): "\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 } diff --git a/shotcut/agent-harness/cli_anything/shotcut/utils/repl_skin.py b/shotcut/agent-harness/cli_anything/shotcut/utils/repl_skin.py index 47260bebd0..6f918f96a2 100644 --- a/shotcut/agent-harness/cli_anything/shotcut/utils/repl_skin.py +++ b/shotcut/agent-harness/cli_anything/shotcut/utils/repl_skin.py @@ -7,7 +7,7 @@ from cli_anything..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") @@ -97,7 +97,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,10 +105,23 @@ def __init__(self, software: str, version: str = "1.0.0", version: CLI version string. history_file: Path for persistent command history. Defaults to ~/.cli-anything-/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//utils/repl_skin.py (this file) + # cli_anything//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 @@ -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). @@ -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 = ""): @@ -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 @@ -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 @@ -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 ──────────────────────────────── diff --git a/slay_the_spire_ii/agent-harness/cli_anything/slay_the_spire_ii/utils/repl_skin.py b/slay_the_spire_ii/agent-harness/cli_anything/slay_the_spire_ii/utils/repl_skin.py index c7312348a7..6f918f96a2 100644 --- a/slay_the_spire_ii/agent-harness/cli_anything/slay_the_spire_ii/utils/repl_skin.py +++ b/slay_the_spire_ii/agent-harness/cli_anything/slay_the_spire_ii/utils/repl_skin.py @@ -155,52 +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 = "" - - # 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 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). @@ -210,28 +176,15 @@ def prompt(self, project_name: str = "", modified: bool = False, Returns: Formatted prompt string. """ - parts = [] + parts = [self.software] - # 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(f"[{ctx}{mod}]") - parts.append(self._c(_GRAY, " ❯ ")) - - return "".join(parts) + parts.append(">") + return " ".join(parts) + " " def prompt_tokens(self, project_name: str = "", modified: bool = False, context: str = ""): @@ -242,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 @@ -255,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 @@ -416,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 ──────────────────────────────── diff --git a/videocaptioner/agent-harness/cli_anything/videocaptioner/utils/repl_skin.py b/videocaptioner/agent-harness/cli_anything/videocaptioner/utils/repl_skin.py index b356cb8357..6f918f96a2 100644 --- a/videocaptioner/agent-harness/cli_anything/videocaptioner/utils/repl_skin.py +++ b/videocaptioner/agent-harness/cli_anything/videocaptioner/utils/repl_skin.py @@ -6,14 +6,14 @@ Usage: from cli_anything..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 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() """ @@ -47,7 +47,6 @@ "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 @@ -98,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", "ollama"). + software: Software name (e.g., "gimp", "shotcut", "blender"). version: CLI version string. history_file: Path for persistent command history. Defaults to ~/.cli-anything-/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//utils/repl_skin.py (this file) + # cli_anything//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 @@ -143,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 · Ollama - 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). @@ -188,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 = ""): @@ -220,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 @@ -233,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 @@ -394,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 ──────────────────────────────── @@ -496,5 +465,4 @@ def toolbar(): "\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 } diff --git a/wiremock/agent-harness/cli_anything/wiremock/utils/repl_skin.py b/wiremock/agent-harness/cli_anything/wiremock/utils/repl_skin.py index c77b64f241..6f918f96a2 100644 --- a/wiremock/agent-harness/cli_anything/wiremock/utils/repl_skin.py +++ b/wiremock/agent-harness/cli_anything/wiremock/utils/repl_skin.py @@ -155,52 +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 = "" - - # 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 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). @@ -210,28 +176,15 @@ def prompt(self, project_name: str = "", modified: bool = False, Returns: Formatted prompt string. """ - parts = [] + parts = [self.software] - # 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(f"[{ctx}{mod}]") - parts.append(self._c(_GRAY, " ❯ ")) - - return "".join(parts) + parts.append(">") + return " ".join(parts) + " " def prompt_tokens(self, project_name: str = "", modified: bool = False, context: str = ""): @@ -242,10 +195,7 @@ def prompt_tokens(self, project_name: str = "", modified: bool = False, Returns: list of (style, text) tuples for prompt_toolkit. """ - 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 @@ -254,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 @@ -399,6 +349,7 @@ def pad(text: str, width: int) -> str: 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) @@ -414,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 ──────────────────────────────── diff --git a/zoom/agent-harness/cli_anything/zoom/utils/repl_skin.py b/zoom/agent-harness/cli_anything/zoom/utils/repl_skin.py index 1d0213c9f6..6f918f96a2 100644 --- a/zoom/agent-harness/cli_anything/zoom/utils/repl_skin.py +++ b/zoom/agent-harness/cli_anything/zoom/utils/repl_skin.py @@ -7,7 +7,7 @@ from cli_anything..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") @@ -47,7 +47,6 @@ "obs_studio": "\033[38;5;55m", # purple "kdenlive": "\033[38;5;69m", # slate blue "shotcut": "\033[38;5;35m", # teal green - "zoom": "\033[38;5;27m", # zoom blue } _DEFAULT_ACCENT = "\033[38;5;75m" # default sky blue @@ -98,7 +97,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,10 +105,23 @@ def __init__(self, software: str, version: str = "1.0.0", version: CLI version string. history_file: Path for persistent command history. Defaults to ~/.cli-anything-/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//utils/repl_skin.py (this file) + # cli_anything//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 @@ -143,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). @@ -188,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 = ""): @@ -220,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 @@ -233,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 @@ -394,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 ──────────────────────────────── @@ -496,5 +465,4 @@ def toolbar(): "\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;27m": "#005fff", # zoom blue } diff --git a/zotero/agent-harness/cli_anything/zotero/utils/repl_skin.py b/zotero/agent-harness/cli_anything/zotero/utils/repl_skin.py index c7312348a7..6f918f96a2 100644 --- a/zotero/agent-harness/cli_anything/zotero/utils/repl_skin.py +++ b/zotero/agent-harness/cli_anything/zotero/utils/repl_skin.py @@ -155,52 +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 = "" - - # 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 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). @@ -210,28 +176,15 @@ def prompt(self, project_name: str = "", modified: bool = False, Returns: Formatted prompt string. """ - parts = [] + parts = [self.software] - # 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(f"[{ctx}{mod}]") - parts.append(self._c(_GRAY, " ❯ ")) - - return "".join(parts) + parts.append(">") + return " ".join(parts) + " " def prompt_tokens(self, project_name: str = "", modified: bool = False, context: str = ""): @@ -242,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 @@ -255,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 @@ -416,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 ────────────────────────────────