chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:55:23 +08:00
commit bc7eac8151
1701 changed files with 376767 additions and 0 deletions
@@ -0,0 +1,62 @@
# Dify Workflow Harness Architecture
## Overview
This harness adds `cli-anything-dify-workflow`, a CLI-Anything wrapper around the
existing `dify-workflow` CLI from the open-source
`dify-ai-workflow-tools` project.
The upstream CLI already provides the real workflow authoring engine for Dify DSL
files. This harness focuses on:
- CLI-Anything packaging under the shared `cli_anything` namespace
- AI-discoverable `SKILL.md`
- unified REPL skin
- registry integration for CLI-Hub
- tests that verify wrapper discovery and forwarding behavior
## Interaction Model
```text
AI Agent
-> cli-anything-dify-workflow
-> installed dify-workflow CLI / dify_workflow Python package
-> local Dify YAML/JSON DSL files
```
## Why a Wrapper Harness
The upstream project is already a mature CLI and does not need to be rewritten
inside CLI-Anything. Following existing wrapper patterns in this repository,
this harness exposes the upstream CLI through a CLI-Anything package so agents
can discover it from CLI-Hub and load its skill metadata.
## Requirements
- Python 3.10+
- Upstream Dify workflow CLI installed separately:
- recommended: install from GitHub
- optional: install from PyPI if published later
## Command Surface
The wrapper exposes the upstream command groups:
- `guide`
- `list-node-types`
- `create`
- `inspect`
- `validate`
- `checklist`
- `edit <subcommand>`
- `config <subcommand>`
- `export`
- `import`
- `diff`
- `layout`
## Testing Strategy
- `test_core.py`: backend discovery, command building, wrapper metadata
- `test_full_e2e.py`: wrapper subprocess smoke tests and workflow lifecycle
forwarding when the upstream CLI is installed
+2
View File
@@ -0,0 +1,2 @@
include DIFY_WORKFLOW.md
recursive-include cli_anything *.md
@@ -0,0 +1,53 @@
# cli-anything-dify-workflow
CLI-Anything wrapper for the open-source Dify workflow DSL editor.
This package does not reimplement the Dify workflow engine. It wraps the
existing `dify-workflow` CLI so agents can discover and use it through the
CLI-Anything ecosystem and CLI-Hub.
## Install
Install the upstream Dify workflow CLI first:
```bash
python -m pip install "dify-ai-workflow-tools @ git+https://github.com/Akabane71/dify-workflow-cli.git@main"
```
Then install this harness:
```bash
pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=dify-workflow/agent-harness
```
If the upstream project is published to PyPI later, you can replace the first
command with a normal PyPI install.
## Usage
```bash
cli-anything-dify-workflow guide
cli-anything-dify-workflow list-node-types
cli-anything-dify-workflow create -o workflow.yaml --template llm
cli-anything-dify-workflow inspect workflow.yaml -j
cli-anything-dify-workflow validate workflow.yaml -j
cli-anything-dify-workflow edit add-node -f workflow.yaml --type code --title "Process"
```
## Notes
- Most upstream commands support `-j` / `--json-output`.
- The wrapper forwards commands to the installed upstream CLI.
- If `dify-workflow` is not on PATH but the `dify_workflow` Python package is
installed, the wrapper falls back to `python -m dify_workflow.cli`.
## Run Tests
```bash
python -m pytest cli_anything/dify_workflow/tests/ -v
```
## Safety Notes
- This harness only edits local Dify workflow DSL files.
- Review generated YAML/JSON before importing into production Dify projects.
@@ -0,0 +1,3 @@
"""CLI-Anything Dify Workflow wrapper package."""
__version__ = "0.1.0"
@@ -0,0 +1,5 @@
from cli_anything.dify_workflow.dify_workflow_cli import main
if __name__ == "__main__":
main()
@@ -0,0 +1,247 @@
#!/usr/bin/env python3
"""CLI-Anything wrapper for the upstream dify-workflow CLI."""
from __future__ import annotations
import shlex
import sys
import click
from cli_anything.dify_workflow import __version__
from cli_anything.dify_workflow.utils.dify_workflow_backend import run_dify_workflow
from cli_anything.dify_workflow.utils.repl_skin import ReplSkin
PASS_ARGS = {
"ignore_unknown_options": True,
"allow_extra_args": True,
}
def _configure_stdio() -> None:
"""Prefer UTF-8 stdio so forwarded output stays readable on Windows."""
for stream in (sys.stdout, sys.stderr):
reconfigure = getattr(stream, "reconfigure", None)
if reconfigure is None:
continue
try:
reconfigure(encoding="utf-8")
except ValueError:
pass
_configure_stdio()
def _emit(output: str) -> None:
if output:
click.echo(output)
def _forward(*prefix: str) -> None:
try:
output = run_dify_workflow([*prefix, *list(click.get_current_context().args)])
except RuntimeError as exc:
raise click.ClickException(str(exc)) from exc
_emit(output)
@click.group(invoke_without_command=True, context_settings=PASS_ARGS)
@click.version_option(version=__version__, prog_name="cli-anything-dify-workflow")
@click.pass_context
def cli(ctx: click.Context) -> None:
"""CLI-Anything wrapper for the Dify workflow DSL editor."""
if ctx.invoked_subcommand is None:
ctx.invoke(repl)
@cli.command()
def repl() -> None:
"""Start a lightweight forwarding REPL."""
skin = ReplSkin("dify_workflow", version=__version__)
skin.print_banner()
skin.info("Type upstream dify-workflow commands without the binary name.")
skin.info("Examples: guide | list-node-types | create -o app.yaml --template llm")
skin.hint("Type help for wrapper commands, quit to exit.")
commands = {
"guide": "Show the upstream tutorial",
"list-node-types": "List supported Dify node types",
"create": "Create a new Dify app",
"inspect": "Inspect a workflow file",
"validate": "Validate a workflow file",
"edit": "Workflow graph mutation commands",
"config": "Model config mutation commands",
"export": "Export YAML or JSON",
"import": "Import and normalize a workflow file",
"diff": "Compare two workflow files",
"layout": "Auto-layout nodes",
}
pt_session = skin.create_prompt_session()
while True:
try:
line = skin.get_input(pt_session, context="dify")
except (EOFError, KeyboardInterrupt):
skin.print_goodbye()
return
if not line:
continue
if line in {"quit", "exit"}:
skin.print_goodbye()
return
if line == "help":
skin.help(commands)
continue
try:
output = run_dify_workflow(shlex.split(line))
except RuntimeError as exc:
skin.error(str(exc))
continue
if output:
click.echo(output)
@cli.command(context_settings=PASS_ARGS)
def guide() -> None:
"""Forward to dify-workflow guide."""
_forward("guide")
@cli.command("list-node-types", context_settings=PASS_ARGS)
def list_node_types() -> None:
"""Forward to dify-workflow list-node-types."""
_forward("list-node-types")
@cli.command(context_settings=PASS_ARGS)
def create() -> None:
"""Forward to dify-workflow create."""
_forward("create")
@cli.command(context_settings=PASS_ARGS)
def inspect() -> None:
"""Forward to dify-workflow inspect."""
_forward("inspect")
@cli.command(context_settings=PASS_ARGS)
def validate() -> None:
"""Forward to dify-workflow validate."""
_forward("validate")
@cli.command(context_settings=PASS_ARGS)
def checklist() -> None:
"""Forward to dify-workflow checklist."""
_forward("checklist")
@cli.command(context_settings=PASS_ARGS)
def export() -> None:
"""Forward to dify-workflow export."""
_forward("export")
@cli.command("import", context_settings=PASS_ARGS)
def import_cmd() -> None:
"""Forward to dify-workflow import."""
_forward("import")
@cli.command(context_settings=PASS_ARGS)
def diff() -> None:
"""Forward to dify-workflow diff."""
_forward("diff")
@cli.command(context_settings=PASS_ARGS)
def layout() -> None:
"""Forward to dify-workflow layout."""
_forward("layout")
@cli.group(context_settings=PASS_ARGS)
def edit() -> None:
"""Workflow graph editing commands."""
@edit.command("add-node", context_settings=PASS_ARGS)
def edit_add_node() -> None:
_forward("edit", "add-node")
@edit.command("remove-node", context_settings=PASS_ARGS)
def edit_remove_node() -> None:
_forward("edit", "remove-node")
@edit.command("update-node", context_settings=PASS_ARGS)
def edit_update_node() -> None:
_forward("edit", "update-node")
@edit.command("add-edge", context_settings=PASS_ARGS)
def edit_add_edge() -> None:
_forward("edit", "add-edge")
@edit.command("remove-edge", context_settings=PASS_ARGS)
def edit_remove_edge() -> None:
_forward("edit", "remove-edge")
@edit.command("set-title", context_settings=PASS_ARGS)
def edit_set_title() -> None:
_forward("edit", "set-title")
@cli.group(context_settings=PASS_ARGS)
def config() -> None:
"""Chat/agent/completion config commands."""
@config.command("set-model", context_settings=PASS_ARGS)
def config_set_model() -> None:
_forward("config", "set-model")
@config.command("set-prompt", context_settings=PASS_ARGS)
def config_set_prompt() -> None:
_forward("config", "set-prompt")
@config.command("add-variable", context_settings=PASS_ARGS)
def config_add_variable() -> None:
_forward("config", "add-variable")
@config.command("set-opening", context_settings=PASS_ARGS)
def config_set_opening() -> None:
_forward("config", "set-opening")
@config.command("add-question", context_settings=PASS_ARGS)
def config_add_question() -> None:
_forward("config", "add-question")
@config.command("add-tool", context_settings=PASS_ARGS)
def config_add_tool() -> None:
_forward("config", "add-tool")
@config.command("remove-tool", context_settings=PASS_ARGS)
def config_remove_tool() -> None:
_forward("config", "remove-tool")
def main() -> None:
cli()
if __name__ == "__main__":
main()
@@ -0,0 +1,56 @@
---
name: cli-anything-dify-workflow
description: Wrapper for the Dify workflow DSL CLI. Create, inspect, validate, edit, and export Dify workflow files through a CLI-Anything harness.
---
# Dify Workflow CLI Skill
## Installation
Install the upstream Dify workflow CLI first:
```bash
python -m pip install "dify-ai-workflow-tools @ git+https://github.com/Akabane71/dify-workflow-cli.git@main"
```
Then install the CLI-Anything harness:
```bash
pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=dify-workflow/agent-harness
```
## Usage
The harness forwards to the upstream `dify-workflow` CLI.
```bash
cli-anything-dify-workflow guide
cli-anything-dify-workflow list-node-types
cli-anything-dify-workflow create -o workflow.yaml --mode workflow --template llm
cli-anything-dify-workflow inspect workflow.yaml -j
cli-anything-dify-workflow validate workflow.yaml -j
cli-anything-dify-workflow edit add-node -f workflow.yaml --type code --title "Process"
cli-anything-dify-workflow config set-model -f app.yaml --provider openai --name gpt-4o
```
## Command Groups
- `guide`
- `list-node-types`
- `create`
- `inspect`
- `validate`
- `checklist`
- `edit add-node|remove-node|update-node|add-edge|remove-edge|set-title`
- `config set-model|set-prompt|add-variable|set-opening|add-question|add-tool|remove-tool`
- `export`
- `import`
- `diff`
- `layout`
## Agent Guidance
- Prefer `-j` / `--json-output` on upstream commands when available.
- The harness itself is a wrapper. Real workflow logic is provided by the upstream project.
- If `dify-workflow` is not on PATH but the `dify_workflow` Python package is installed, the wrapper falls back to `python -m dify_workflow.cli`.
- All operations are local file edits on Dify YAML/JSON DSL files.
@@ -0,0 +1,55 @@
# Dify Workflow Harness Test Documentation
## Test Inventory Plan
- `test_core.py`: backend discovery, command construction, packaging fixture checks
- `test_full_e2e.py`: wrapper subprocess smoke tests and workflow lifecycle forwarding
## Unit Test Plan
### utils/dify_workflow_backend.py
- resolve the upstream `dify-workflow` executable from PATH
- fall back to `python -m dify_workflow.cli` when only the Python package exists
- raise clear install guidance when neither exists
- build the final subprocess command correctly
### Packaging fixtures
- README documents two-step installation
- SKILL.md documents wrapper semantics and installation
- wrapper CLI exposes the expected top-level commands
## E2E Test Plan
- verify `--help` on the wrapper command
- create a minimal workflow through the wrapper when the upstream CLI is installed
- validate the created workflow through the wrapper
- inspect the workflow as JSON through the wrapper
## Notes
This harness wraps an external open-source CLI rather than shipping the workflow
engine itself. Full E2E tests therefore require the upstream Dify workflow CLI
to be installed locally before the suite is run. If the upstream CLI is missing
or broken, the E2E suite now fails explicitly instead of being skipped.
## Test Results
Verified on 2026-04-07 in the local CLI-Anything worktree.
### Commands Run
```bash
uv run --with pytest --with click --with prompt-toolkit --with-editable "c:\Users\lishun\py_ws\提交pr\dify-workflow-cli" python -m pytest cli_anything/dify_workflow/tests -vv -s
```
### Results
- total collected: 11
- passed: 11
### Notes
- Full forwarding tests were rerun with the upstream `dify-workflow` package installed editable from the local `dify-workflow-cli` repository.
- The wrapper suite now exercises `help`, `create`, `validate`, and `inspect -j` end-to-end against the real upstream CLI.
@@ -0,0 +1 @@
"""Tests for the Dify Workflow harness."""
@@ -0,0 +1,67 @@
import importlib.util
from pathlib import Path
from unittest.mock import patch
import pytest
from click.testing import CliRunner
from cli_anything.dify_workflow.dify_workflow_cli import cli
from cli_anything.dify_workflow.utils.dify_workflow_backend import (
build_command,
has_upstream_cli,
require_dify_workflow_command,
)
class TestBackendDiscovery:
def test_require_command_returns_binary_path(self):
with patch("cli_anything.dify_workflow.utils.dify_workflow_backend.shutil.which", return_value="/usr/bin/dify-workflow"):
assert require_dify_workflow_command() == ["/usr/bin/dify-workflow"]
def test_require_command_falls_back_to_python_module(self):
with patch("cli_anything.dify_workflow.utils.dify_workflow_backend.shutil.which", return_value=None), patch(
"cli_anything.dify_workflow.utils.dify_workflow_backend.importlib.util.find_spec",
return_value=object(),
), patch("cli_anything.dify_workflow.utils.dify_workflow_backend.sys.executable", "python"):
assert require_dify_workflow_command() == ["python", "-m", "dify_workflow.cli"]
def test_require_command_raises_install_guidance(self):
with patch("cli_anything.dify_workflow.utils.dify_workflow_backend.shutil.which", return_value=None), patch(
"cli_anything.dify_workflow.utils.dify_workflow_backend.importlib.util.find_spec",
return_value=None,
):
with pytest.raises(RuntimeError, match="dify-workflow command not found"):
require_dify_workflow_command()
def test_build_command_appends_args(self):
with patch("cli_anything.dify_workflow.utils.dify_workflow_backend.require_dify_workflow_command", return_value=["dify-workflow"]):
assert build_command(["guide", "-j"]) == ["dify-workflow", "guide", "-j"]
def test_has_upstream_cli_false_when_resolution_fails(self):
with patch("cli_anything.dify_workflow.utils.dify_workflow_backend.require_dify_workflow_command", side_effect=RuntimeError("missing")):
assert has_upstream_cli() is False
class TestWrapperCLI:
def test_help_renders(self):
runner = CliRunner()
result = runner.invoke(cli, ["--help"])
assert result.exit_code == 0
assert "CLI-Anything wrapper for the Dify workflow DSL editor" in result.output
assert "edit" in result.output
assert "config" in result.output
class TestPackagingFixtures:
def test_readme_documents_two_step_install(self):
package_root = Path(__file__).resolve().parents[1]
readme = (package_root / "README.md").read_text(encoding="utf-8")
assert "Install the upstream Dify workflow CLI first" in readme
assert "git+https://github.com/Akabane71/dify-workflow-cli.git@main" in readme
def test_skill_file_documents_wrapper_behavior(self):
package_root = Path(__file__).resolve().parents[1]
skill = (package_root / "skills" / "SKILL.md").read_text(encoding="utf-8")
assert "wrapper" in skill.lower()
assert "cli-anything-dify-workflow" in skill
assert "dify-workflow" in skill
@@ -0,0 +1,84 @@
import json
import os
import shutil
import subprocess
import sys
from cli_anything.dify_workflow.utils.dify_workflow_backend import require_dify_workflow_command
def _decode_output(data: bytes | None) -> str:
if not data:
return ""
return data.decode("utf-8", errors="replace")
def _require_working_upstream() -> None:
command = require_dify_workflow_command()
result = subprocess.run(
command + ["--help"],
capture_output=True,
check=False,
)
if result.returncode != 0:
stderr = _decode_output(result.stderr)
stdout = _decode_output(result.stdout)
raise RuntimeError(
"upstream dify-workflow CLI is required for wrapper E2E tests and must respond to --help.\n"
f"stdout: {stdout}\n"
f"stderr: {stderr}"
)
def _resolve_cli(name: str, module: str) -> list[str]:
force = os.environ.get("CLI_ANYTHING_FORCE_INSTALLED", "").strip() == "1"
path = shutil.which(name)
if path:
return [path]
if force:
raise RuntimeError(f"{name} not found in PATH. Install with: pip install -e .")
return [sys.executable, "-m", module]
class TestWrapperE2E:
CLI_BASE = _resolve_cli("cli-anything-dify-workflow", "cli_anything.dify_workflow")
@classmethod
def setup_class(cls):
_require_working_upstream()
def _run(self, args, check=True):
result = subprocess.run(self.CLI_BASE + args, capture_output=True, check=False)
stdout = _decode_output(result.stdout)
stderr = _decode_output(result.stderr)
if check and result.returncode != 0:
raise subprocess.CalledProcessError(result.returncode, result.args, output=stdout, stderr=stderr)
result.stdout = stdout
result.stderr = stderr
return result
def test_help(self):
result = self._run(["--help"])
assert result.returncode == 0
assert "create" in result.stdout
def test_create_and_validate_workflow(self, tmp_path):
workflow_path = tmp_path / "workflow.yaml"
result = self._run(["create", "-o", str(workflow_path), "--template", "llm", "-j"])
assert result.returncode == 0, result.stderr
data = json.loads(result.stdout)
assert data["status"] == "created"
result = self._run(["validate", str(workflow_path), "-j"])
assert result.returncode == 0, result.stderr
report = json.loads(result.stdout)
assert report["valid"] is True
def test_inspect_json(self, tmp_path):
workflow_path = tmp_path / "workflow.yaml"
self._run(["create", "-o", str(workflow_path)], check=True)
result = self._run(["inspect", str(workflow_path), "-j"])
assert result.returncode == 0, result.stderr
assert result.stdout.strip().startswith("{")
@@ -0,0 +1 @@
"""Utility helpers for the Dify Workflow wrapper."""
@@ -0,0 +1,59 @@
"""Backend adapter for the external dify-workflow CLI."""
from __future__ import annotations
import importlib.util
import shutil
import subprocess
import sys
def _decode_output(data: bytes | None) -> str:
"""Decode subprocess bytes predictably across Windows locales."""
if not data:
return ""
return data.decode("utf-8", errors="replace")
def require_dify_workflow_command() -> list[str]:
"""Resolve the upstream dify-workflow executable or module."""
cli_path = shutil.which("dify-workflow")
if cli_path:
return [cli_path]
if importlib.util.find_spec("dify_workflow") is not None:
return [sys.executable, "-m", "dify_workflow.cli"]
raise RuntimeError(
"dify-workflow command not found. Install the upstream project first with:\n"
" python -m pip install \"dify-ai-workflow-tools @ git+https://github.com/Akabane71/dify-workflow-cli.git@main\"\n"
"If you later publish to PyPI, a normal pip install is also fine."
)
def build_command(args: list[str]) -> list[str]:
"""Build the final subprocess command."""
return [*require_dify_workflow_command(), *args]
def has_upstream_cli() -> bool:
"""Return whether the upstream CLI or module is available."""
try:
require_dify_workflow_command()
except RuntimeError:
return False
return True
def run_dify_workflow(args: list[str]) -> str:
"""Run the upstream CLI and return stdout."""
command = build_command(args)
result = subprocess.run(
command,
capture_output=True,
check=False,
)
if result.returncode != 0:
message = (_decode_output(result.stderr) or _decode_output(result.stdout)).strip()
raise RuntimeError(message or "dify-workflow exited with a non-zero status")
return _decode_output(result.stdout).rstrip()
@@ -0,0 +1,567 @@
"""cli-anything REPL Skin — Unified terminal interface for all CLI harnesses.
Copy this file into your CLI package at:
cli_anything/<software>/utils/repl_skin.py
Usage:
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("shotcut", version="1.0.0")
skin.print_banner() # auto-detects repo-root or packaged SKILL.md
prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
skin.success("Project saved")
skin.error("File not found")
skin.warning("Unsaved changes")
skin.info("Processing 24 clips...")
skin.status("Track 1", "3 clips, 00:02:30")
skin.table(headers, rows)
skin.print_goodbye()
"""
import os
import sys
from pathlib import Path
# ── ANSI color codes (no external deps for core styling) ──────────────
_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"
_SKILL_SOURCE_REPO = os.environ.get("CLI_ANYTHING_SKILL_REPO", "HKUDS/CLI-Anything")
# ── Brand icon ────────────────────────────────────────────────────────
# The cli-anything icon: a small colored diamond/chevron mark
_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))
def _display_home_path(path: str) -> str:
"""Display a path relative to the home directory when possible."""
expanded = Path(path).expanduser().resolve()
home = Path.home().resolve()
try:
relative = expanded.relative_to(home)
return f"~/{relative.as_posix()}"
except ValueError:
return str(expanded)
class ReplSkin:
"""Unified REPL skin for cli-anything CLIs.
Provides consistent branding, prompts, and message formatting
across all CLI harnesses built with the cli-anything methodology.
"""
def __init__(self, software: str, version: str = "1.0.0",
history_file: str | None = None, skill_path: str | None = None):
"""Initialize the REPL skin.
Args:
software: Software name (e.g., "gimp", "shotcut", "blender").
version: CLI version string.
history_file: Path for persistent command history.
Defaults to ~/.cli-anything-<software>/history
skill_path: Path to the SKILL.md file for agent discovery.
Auto-detected from the repo-root skills/ tree when present,
otherwise from the package's skills/ directory.
Displayed in banner for AI agents to know where to read skill info.
"""
self.software = software.lower().replace("-", "_")
self.display_name = software.replace("_", " ").title()
self.version = version
software_aliases = {"iterm2_ctl": "iterm2"}
self.skill_slug = software_aliases.get(self.software, self.software).replace("_", "-")
self.skill_id = f"cli-anything-{self.skill_slug}"
self.skill_install_cmd = (
f"npx skills add {_SKILL_SOURCE_REPO} --skill {self.skill_id} -g -y"
)
global_skill_root = Path(
os.environ.get("CLI_ANYTHING_GLOBAL_SKILLS_DIR", str(Path.home() / ".agents" / "skills"))
).expanduser()
self.global_skill_path = str(global_skill_root / self.skill_id / "SKILL.md")
# Prefer repo-root canonical skills/<skill-id>/SKILL.md when running
# inside the CLI-Anything monorepo. Fall back to the packaged
# cli_anything/<software>/skills/SKILL.md for installed harnesses.
if skill_path is None:
package_skill = Path(__file__).resolve().parent.parent / "skills" / "SKILL.md"
repo_skill = None
for parent in Path(__file__).resolve().parents:
candidate = parent / "skills" / self.skill_id / "SKILL.md"
if candidate.is_file():
repo_skill = candidate
break
if repo_skill and repo_skill.is_file():
skill_path = str(repo_skill)
elif package_skill.is_file():
skill_path = str(package_skill)
self.skill_path = skill_path
self.accent = _ACCENT_COLORS.get(self.software, _DEFAULT_ACCENT)
# History file
if history_file is None:
hist_dir = Path.home() / f".cli-anything-{self.software}"
hist_dir.mkdir(parents=True, exist_ok=True)
self.history_file = str(hist_dir / "history")
else:
self.history_file = history_file
# Detect terminal capabilities
self._color = self._detect_color_support()
def _detect_color_support(self) -> bool:
"""Check if terminal supports color."""
if os.environ.get("NO_COLOR"):
return False
if os.environ.get("CLI_ANYTHING_NO_COLOR"):
return False
if not hasattr(sys.stdout, "isatty"):
return False
return sys.stdout.isatty()
def _c(self, code: str, text: str) -> str:
"""Apply color code if colors are supported."""
if not self._color:
return text
return f"{code}{text}{_RESET}"
# ── Banner ────────────────────────────────────────────────────────
def print_banner(self):
"""Print the startup banner with branding."""
import textwrap
inner = 72
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}"
def _meta_lines(label: str, value: str) -> list[str]:
"""Wrap a metadata line for the banner box."""
icon = self._c(_MAGENTA, "")
label_text = self._c(_DARK_GRAY, label)
prefix = f" {icon} {label_text} "
available = max(12, inner - _visible_len(prefix))
wrapped = textwrap.wrap(
value,
width=available,
break_long_words=True,
break_on_hyphens=False,
) or [""]
lines = [f"{prefix}{self._c(_LIGHT_GRAY, wrapped[0])}"]
continuation_prefix = " " * _visible_len(prefix)
for chunk in wrapped[1:]:
lines.append(f"{continuation_prefix}{self._c(_LIGHT_GRAY, chunk)}")
return lines
top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
# Title: ◆ cli-anything · 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 = ""
meta_lines: list[str] = []
meta_lines.extend(_meta_lines("Install:", self.skill_install_cmd))
meta_lines.extend(_meta_lines("Global skill:", _display_home_path(self.global_skill_path)))
print(top)
print(_box_line(title))
print(_box_line(ver))
for line in meta_lines:
print(_box_line(line))
print(_box_line(empty))
print(_box_line(tip))
print(bot)
print()
# ── Prompt ────────────────────────────────────────────────────────
def prompt(self, project_name: str = "", modified: bool = False,
context: str = "") -> str:
"""Build a styled prompt string for prompt_toolkit or input().
Args:
project_name: Current project name (empty if none open).
modified: Whether the project has unsaved changes.
context: Optional extra context to show in prompt.
Returns:
Formatted prompt string.
"""
parts = []
# Icon
if self._color:
parts.append(f"{_CYAN}{_RESET} ")
else:
parts.append("> ")
# Software name
parts.append(self._c(self.accent + _BOLD, self.software))
# Project context
if project_name or context:
ctx = context or project_name
mod = "*" if modified else ""
parts.append(f" {self._c(_DARK_GRAY, '[')}")
parts.append(self._c(_LIGHT_GRAY, f"{ctx}{mod}"))
parts.append(self._c(_DARK_GRAY, ']'))
parts.append(self._c(_GRAY, " "))
return "".join(parts)
def prompt_tokens(self, project_name: str = "", modified: bool = False,
context: str = ""):
"""Build prompt_toolkit formatted text tokens for the prompt.
Use with prompt_toolkit's FormattedText for proper ANSI handling.
Returns:
list of (style, text) tuples for prompt_toolkit.
"""
accent_hex = _ANSI_256_TO_HEX.get(self.accent, "#5fafff")
tokens = []
tokens.append(("class:icon", ""))
tokens.append(("class:software", self.software))
if project_name or context:
ctx = context or project_name
mod = "*" if modified else ""
tokens.append(("class:bracket", " ["))
tokens.append(("class:context", f"{ctx}{mod}"))
tokens.append(("class:bracket", "]"))
tokens.append(("class:arrow", " "))
return tokens
def get_prompt_style(self):
"""Get a prompt_toolkit Style object matching the skin.
Returns:
prompt_toolkit.styles.Style
"""
try:
from prompt_toolkit.styles import Style
except ImportError:
return None
accent_hex = _ANSI_256_TO_HEX.get(self.accent, "#5fafff")
return Style.from_dict({
"icon": "#5fdfdf bold", # cyan brand color
"software": f"{accent_hex} bold",
"bracket": "#585858",
"context": "#bcbcbc",
"arrow": "#808080",
# Completion menu
"completion-menu.completion": "bg:#303030 #bcbcbc",
"completion-menu.completion.current": f"bg:{accent_hex} #000000",
"completion-menu.meta.completion": "bg:#303030 #808080",
"completion-menu.meta.completion.current": f"bg:{accent_hex} #000000",
# Auto-suggest
"auto-suggest": "#585858",
# Bottom toolbar
"bottom-toolbar": "bg:#1c1c1c #808080",
"bottom-toolbar.text": "#808080",
})
# ── Messages ──────────────────────────────────────────────────────
def success(self, message: str):
"""Print a success message with green checkmark."""
icon = self._c(_GREEN + _BOLD, "")
print(f" {icon} {self._c(_GREEN, message)}")
def error(self, message: str):
"""Print an error message with red cross."""
icon = self._c(_RED + _BOLD, "")
print(f" {icon} {self._c(_RED, message)}", file=sys.stderr)
def warning(self, message: str):
"""Print a warning message with yellow triangle."""
icon = self._c(_YELLOW + _BOLD, "")
print(f" {icon} {self._c(_YELLOW, message)}")
def info(self, message: str):
"""Print an info message with blue dot."""
icon = self._c(_BLUE, "")
print(f" {icon} {self._c(_LIGHT_GRAY, message)}")
def hint(self, message: str):
"""Print a subtle hint message."""
print(f" {self._c(_DARK_GRAY, message)}")
def section(self, title: str):
"""Print a section header."""
print()
print(f" {self._c(self.accent + _BOLD, title)}")
print(f" {self._c(_DARK_GRAY, _H_LINE * len(title))}")
# ── Status display ────────────────────────────────────────────────
def status(self, label: str, value: str):
"""Print a key-value status line."""
lbl = self._c(_GRAY, f" {label}:")
val = self._c(_WHITE, f" {value}")
print(f"{lbl}{val}")
def status_block(self, items: dict[str, str], title: str = ""):
"""Print a block of status key-value pairs.
Args:
items: Dict of label -> value pairs.
title: Optional title for the block.
"""
if title:
self.section(title)
max_key = max(len(k) for k in items) if items else 0
for label, value in items.items():
lbl = self._c(_GRAY, f" {label:<{max_key}}")
val = self._c(_WHITE, f" {value}")
print(f"{lbl}{val}")
def progress(self, current: int, total: int, label: str = ""):
"""Print a simple progress indicator.
Args:
current: Current step number.
total: Total number of steps.
label: Optional label for the progress.
"""
pct = int(current / total * 100) if total > 0 else 0
bar_width = 20
filled = int(bar_width * current / total) if total > 0 else 0
bar = "" * filled + "" * (bar_width - filled)
text = f" {self._c(_CYAN, bar)} {self._c(_GRAY, f'{pct:3d}%')}"
if label:
text += f" {self._c(_LIGHT_GRAY, label)}"
print(text)
# ── Table display ─────────────────────────────────────────────────
def table(self, headers: list[str], rows: list[list[str]],
max_col_width: int = 40):
"""Print a formatted table with box-drawing characters.
Args:
headers: Column header strings.
rows: List of rows, each a list of cell strings.
max_col_width: Maximum column width before truncation.
"""
if not headers:
return
# Calculate column widths
col_widths = [min(len(h), max_col_width) for h in headers]
for row in rows:
for i, cell in enumerate(row):
if i < len(col_widths):
col_widths[i] = min(
max(col_widths[i], len(str(cell))), max_col_width
)
def pad(text: str, width: int) -> str:
t = str(text)[:width]
return t + " " * (width - len(t))
# Header
header_cells = [
self._c(_CYAN + _BOLD, pad(h, col_widths[i]))
for i, h in enumerate(headers)
]
sep = self._c(_DARK_GRAY, f" {_V_LINE} ")
header_line = f" {sep.join(header_cells)}"
print(header_line)
# Separator
sep_parts = [self._c(_DARK_GRAY, _H_LINE * w) for w in col_widths]
sep_line = self._c(_DARK_GRAY, f" {'───'.join([_H_LINE * w for w in col_widths])}")
print(sep_line)
# Rows
for row in rows:
cells = []
for i, cell in enumerate(row):
if i < len(col_widths):
cells.append(self._c(_LIGHT_GRAY, pad(str(cell), col_widths[i])))
row_sep = self._c(_DARK_GRAY, f" {_V_LINE} ")
print(f" {row_sep.join(cells)}")
# ── Help display ──────────────────────────────────────────────────
def help(self, commands: dict[str, str]):
"""Print a formatted help listing.
Args:
commands: Dict of command -> description pairs.
"""
self.section("Commands")
max_cmd = max(len(c) for c in commands) if commands else 0
for cmd, desc in commands.items():
cmd_styled = self._c(self.accent, f" {cmd:<{max_cmd}}")
desc_styled = self._c(_GRAY, f" {desc}")
print(f"{cmd_styled}{desc_styled}")
print()
# ── Goodbye ───────────────────────────────────────────────────────
def print_goodbye(self):
"""Print a styled goodbye message."""
print(f"\n {_ICON_SMALL} {self._c(_GRAY, 'Goodbye!')}\n")
# ── Prompt toolkit session factory ────────────────────────────────
def create_prompt_session(self):
"""Create a prompt_toolkit PromptSession with skin styling.
Returns:
A configured PromptSession, or None if prompt_toolkit unavailable.
"""
try:
from prompt_toolkit import PromptSession
from prompt_toolkit.history import FileHistory
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.formatted_text import FormattedText
style = self.get_prompt_style()
session = PromptSession(
history=FileHistory(self.history_file),
auto_suggest=AutoSuggestFromHistory(),
style=style,
enable_history_search=True,
)
return session
except ImportError:
return None
def get_input(self, pt_session, project_name: str = "",
modified: bool = False, context: str = "") -> str:
"""Get input from user using prompt_toolkit or fallback.
Args:
pt_session: A prompt_toolkit PromptSession (or None).
project_name: Current project name.
modified: Whether project has unsaved changes.
context: Optional context string.
Returns:
User input string (stripped).
"""
if pt_session is not None:
from prompt_toolkit.formatted_text import FormattedText
tokens = self.prompt_tokens(project_name, modified, context)
return pt_session.prompt(FormattedText(tokens)).strip()
else:
raw_prompt = self.prompt(project_name, modified, context)
return input(raw_prompt).strip()
# ── Toolbar builder ───────────────────────────────────────────────
def bottom_toolbar(self, items: dict[str, str]):
"""Create a bottom toolbar callback for prompt_toolkit.
Args:
items: Dict of label -> value pairs to show in toolbar.
Returns:
A callable that returns FormattedText for the toolbar.
"""
def toolbar():
from prompt_toolkit.formatted_text import FormattedText
parts = []
for i, (k, v) in enumerate(items.items()):
if i > 0:
parts.append(("class:bottom-toolbar.text", ""))
parts.append(("class:bottom-toolbar.text", f" {k}: "))
parts.append(("class:bottom-toolbar", v))
return FormattedText(parts)
return toolbar
# ── ANSI 256-color to hex mapping (for prompt_toolkit styles) ─────────
_ANSI_256_TO_HEX = {
"\033[38;5;33m": "#0087ff", # audacity navy blue
"\033[38;5;35m": "#00af5f", # shotcut teal
"\033[38;5;39m": "#00afff", # inkscape bright blue
"\033[38;5;40m": "#00d700", # libreoffice green
"\033[38;5;55m": "#5f00af", # obs purple
"\033[38;5;69m": "#5f87ff", # kdenlive slate blue
"\033[38;5;75m": "#5fafff", # default sky blue
"\033[38;5;80m": "#5fd7d7", # brand cyan
"\033[38;5;208m": "#ff8700", # blender deep orange
"\033[38;5;214m": "#ffaf00", # gimp warm orange
}
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""
setup.py for cli-anything-dify-workflow
Install with: pip install -e .
Or publish to PyPI: python -m build && twine upload dist/*
"""
from pathlib import Path
from setuptools import setup, find_namespace_packages
ROOT = Path(__file__).parent
README = ROOT / "cli_anything/dify_workflow/README.md"
def read_readme() -> str:
try:
return README.read_text(encoding="utf-8")
except FileNotFoundError:
return "CLI-Anything wrapper for the Dify Workflow CLI."
setup(
name="cli-anything-dify-workflow",
version="0.1.0",
author="Akabane71",
description="CLI-Anything wrapper for the Dify Workflow CLI and DSL editor",
long_description=read_readme(),
long_description_content_type="text/markdown",
url="https://github.com/HKUDS/CLI-Anything",
packages=find_namespace_packages(include=["cli_anything.*"]),
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Topic :: Software Development :: Libraries :: Python Modules",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
],
python_requires=">=3.10",
install_requires=[
"click>=8.0.0",
"prompt-toolkit>=3.0.0",
],
extras_require={
"dev": [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
],
},
entry_points={
"console_scripts": [
"cli-anything-dify-workflow=cli_anything.dify_workflow.dify_workflow_cli:main",
],
},
package_data={
"cli_anything.dify_workflow": ["skills/*.md"],
},
include_package_data=True,
zip_safe=False,
)