From b8143127a38a0fe2d1ebca9cf0b4a38ace661e3d Mon Sep 17 00:00:00 2001 From: GangGreenTemperTatum <104169244+GangGreenTemperTatum@users.noreply.github.com> Date: Sun, 29 Mar 2026 08:44:22 -0400 Subject: [PATCH] add uv-powered agent cli --- README.md | 19 ++ p4rs3lt0ngv3_cli/__init__.py | 2 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 206 bytes .../__pycache__/agent.cpython-314.pyc | Bin 0 -> 14393 bytes .../__pycache__/bridge.cpython-314.pyc | Bin 0 -> 7926 bytes .../__pycache__/cli.cpython-314.pyc | Bin 0 -> 9375 bytes .../__pycache__/models.cpython-314.pyc | Bin 0 -> 3770 bytes p4rs3lt0ngv3_cli/agent.py | 250 ++++++++++++++++++ p4rs3lt0ngv3_cli/bridge.py | 148 +++++++++++ p4rs3lt0ngv3_cli/cli.py | 183 +++++++++++++ p4rs3lt0ngv3_cli/models.py | 55 ++++ pyproject.toml | 27 ++ .../test_cli_e2e.cpython-314-pytest-9.0.2.pyc | Bin 0 -> 19050 bytes python_tests/test_cli_e2e.py | 90 +++++++ scripts/cli_bridge.js | 217 +++++++++++++++ uv.lock | 79 ++++++ 16 files changed, 1070 insertions(+) create mode 100644 p4rs3lt0ngv3_cli/__init__.py create mode 100644 p4rs3lt0ngv3_cli/__pycache__/__init__.cpython-314.pyc create mode 100644 p4rs3lt0ngv3_cli/__pycache__/agent.cpython-314.pyc create mode 100644 p4rs3lt0ngv3_cli/__pycache__/bridge.cpython-314.pyc create mode 100644 p4rs3lt0ngv3_cli/__pycache__/cli.cpython-314.pyc create mode 100644 p4rs3lt0ngv3_cli/__pycache__/models.cpython-314.pyc create mode 100644 p4rs3lt0ngv3_cli/agent.py create mode 100644 p4rs3lt0ngv3_cli/bridge.py create mode 100644 p4rs3lt0ngv3_cli/cli.py create mode 100644 p4rs3lt0ngv3_cli/models.py create mode 100644 pyproject.toml create mode 100644 python_tests/__pycache__/test_cli_e2e.cpython-314-pytest-9.0.2.pyc create mode 100644 python_tests/test_cli_e2e.py create mode 100644 scripts/cli_bridge.js create mode 100644 uv.lock diff --git a/README.md b/README.md index 7672fc0..d652878 100644 --- a/README.md +++ b/README.md @@ -314,6 +314,25 @@ models **Alternative — run as a local app (npm / npx):** From the project root, after `npm install` and `npm run build`, use **`npm start`** (runs [`serve`](https://github.com/vercel/serve) on port **8080**) or **`npx serve dist -l 8080`**. Then open **http://localhost:8080** — same UI, stable URL you can bookmark. **`npm run preview`** runs a full **`npm run build`** and then serves **`dist/`** in one step. +### Agent CLI + +This repo also ships a Python CLI that reuses the existing Node transform runtime without changing the static-site workflow. + +```bash +uv run p4rs3lt0ngv3-cli list +uv run p4rs3lt0ngv3-cli inspect caesar --json +uv run p4rs3lt0ngv3-cli encode --transform base64 --text "Hello World" +uv run p4rs3lt0ngv3-cli decode --transform base64 --text "SGVsbG8gV29ybGQ=" +uv run p4rs3lt0ngv3-cli auto-decode --text "SGVsbG8=" +uv run p4rs3lt0ngv3-cli agent "encode 'Attack at dawn' as caesar shift 5" +``` + +Notes: + +- The CLI is managed with **`uv`** via [`pyproject.toml`](pyproject.toml). +- It shells into Node to execute the canonical transforms under `src/transformers/`. +- Existing web build and Node test flows remain unchanged. + ### **Development Setup** ```bash # Install dependencies diff --git a/p4rs3lt0ngv3_cli/__init__.py b/p4rs3lt0ngv3_cli/__init__.py new file mode 100644 index 0000000..9f5cba2 --- /dev/null +++ b/p4rs3lt0ngv3_cli/__init__.py @@ -0,0 +1,2 @@ +"""Python CLI package for P4RS3LT0NGV3.""" + diff --git a/p4rs3lt0ngv3_cli/__pycache__/__init__.cpython-314.pyc b/p4rs3lt0ngv3_cli/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..37ba5fd4621c89ccd1085596b188185f38afe6b5 GIT binary patch literal 206 zcmdPq7)qK~XWzrduZ*f^)eATPblI6gTiQ$IdFGcU6wK3=b&@)n0pZhlH>PO4oI8_*z- V(~3EO#0O?ZM#kF=T16~C4ghiGGO+*v literal 0 HcmV?d00001 diff --git a/p4rs3lt0ngv3_cli/__pycache__/agent.cpython-314.pyc b/p4rs3lt0ngv3_cli/__pycache__/agent.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b13b5cb456f2b335bc6f8b5c81493108169471e2 GIT binary patch literal 14393 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 literal 0 HcmV?d00001 diff --git a/p4rs3lt0ngv3_cli/agent.py b/p4rs3lt0ngv3_cli/agent.py new file mode 100644 index 0000000..ef20209 --- /dev/null +++ b/p4rs3lt0ngv3_cli/agent.py @@ -0,0 +1,250 @@ +from __future__ import annotations + +import difflib +import re +from dataclasses import asdict +from typing import Any + +from .bridge import auto_decode, inspect_transform, list_transforms, run_transform +from .models import AgentStep, TransformInfo + + +TEXT_QUOTE_RE = re.compile(r"""['"](?P[^'"]+)['"]""") + + +def find_transform(query: str) -> TransformInfo | None: + normalized = query.strip().lower().replace("-", "_") + transforms = list_transforms() + + exact = {transform.key: transform for transform in transforms} + if normalized in exact: + return exact[normalized] + + for transform in transforms: + if normalized == transform.name.lower(): + return transform + if normalized in transform.search_tokens: + return transform + + name_map = {transform.key: transform for transform in transforms} + matches = difflib.get_close_matches(normalized, list(name_map), n=1, cutoff=0.55) + if matches: + return name_map[matches[0]] + + token_matches = [] + query_tokens = set(re.findall(r"[a-z0-9_]+", normalized)) + for transform in transforms: + score = len(query_tokens & transform.search_tokens) + if score: + token_matches.append((score, transform.priority, transform)) + if token_matches: + token_matches.sort(key=lambda item: (item[0], item[1]), reverse=True) + return token_matches[0][2] + + return None + + +def coerce_option_value(raw: str) -> Any: + lowered = raw.lower() + if lowered in {"true", "false"}: + return lowered == "true" + if re.fullmatch(r"-?\d+", raw): + return int(raw) + if re.fullmatch(r"-?\d+\.\d+", raw): + return float(raw) + return raw + + +def extract_text(prompt: str) -> str | None: + match = TEXT_QUOTE_RE.search(prompt) + if match: + return match.group("text") + return None + + +def extract_transform_query(prompt: str) -> str | None: + patterns = [ + r"\b(?:as|using|with|via|from|into|to)\s+([a-z0-9 _-]+?)(?:\s+with\b|\s+without\b|$)", + r"\b(?:inspect|show|details for|about)\s+([a-z0-9 _-]+)$", + ] + normalized = prompt.strip().lower() + for pattern in patterns: + match = re.search(pattern, normalized) + if match: + return match.group(1).strip() + return None + + +def extract_option_hints(prompt: str, transform: TransformInfo | None) -> dict[str, Any]: + if not transform: + return {} + + options: dict[str, Any] = {} + normalized = prompt.lower() + for option in transform.configurable_options: + id_pattern = option.id.replace("_", "[ _-]?") + value_match = re.search(rf"\b{id_pattern}\s+([^\s,]+)", normalized) + if value_match: + options[option.id] = coerce_option_value(value_match.group(1)) + continue + + label_tokens = re.findall(r"[a-z0-9]+", option.label.lower()) + if label_tokens: + label_pattern = r"[ _-]?".join(label_tokens) + value_match = re.search(rf"\b{label_pattern}\s+([^\s,]+)", normalized) + if value_match: + options[option.id] = coerce_option_value(value_match.group(1)) + continue + + if option.type == "boolean": + if re.search(rf"\b(?:with|enable)\s+{id_pattern}\b", normalized): + options[option.id] = True + if re.search(rf"\b(?:without|disable|no)\s+{id_pattern}\b", normalized): + options[option.id] = False + + generic_number = re.search(r"\bshift\s+(-?\d+)\b", normalized) + if generic_number and any(option.id == "shift" for option in transform.configurable_options): + options["shift"] = int(generic_number.group(1)) + + return options + + +def split_steps(prompt: str) -> list[str]: + return [segment.strip() for segment in re.split(r"\bthen\b|&&|->", prompt) if segment.strip()] + + +def plan_prompt(prompt: str) -> list[AgentStep]: + steps: list[AgentStep] = [] + segments = split_steps(prompt) + + for index, segment in enumerate(segments): + lowered = segment.lower() + text = extract_text(segment) + + if any(token in lowered for token in ["list", "show transforms", "available transforms", "what can you do"]): + steps.append(AgentStep(kind="list", explanation="List available transforms")) + continue + + if any(token in lowered for token in ["inspect", "details", "about"]) and not any( + token in lowered for token in ["decode", "encode", "transform", "convert"] + ): + transform_query = extract_transform_query(segment) or segment + transform = find_transform(transform_query) + steps.append( + AgentStep( + kind="inspect", + transform_key=transform.key if transform else None, + explanation=f"Inspect transform derived from '{transform_query}'", + ) + ) + continue + + action = None + if any(token in lowered for token in ["decode", "decrypt", "reverse", "undo"]): + action = "decode" + elif any(token in lowered for token in ["preview"]): + action = "preview" + elif any(token in lowered for token in ["encode", "transform", "convert", "make"]): + action = "encode" + + transform_query = extract_transform_query(segment) + transform = find_transform(transform_query) if transform_query else None + + if action == "decode" and transform is None and "from " not in lowered and "using " not in lowered: + steps.append(AgentStep(kind="auto-decode", text=text or segment.strip(), explanation="Use universal decoder")) + continue + + if action and transform: + steps.append( + AgentStep( + kind="run", + transform_key=transform.key, + action=action, + text=text, + options=extract_option_hints(segment, transform), + explanation=f"{action} with {transform.key}", + ) + ) + continue + + if index == 0 and text and transform is None and action == "decode": + steps.append(AgentStep(kind="auto-decode", text=text, explanation="Use universal decoder")) + continue + + raise ValueError(f"Could not resolve request segment: {segment}") + + return steps + + +def execute_plan(prompt: str) -> dict[str, Any]: + steps = plan_prompt(prompt) + current_text: str | None = None + outputs: list[dict[str, Any]] = [] + + for step in steps: + if step.kind == "list": + transforms = list_transforms() + outputs.append( + { + "kind": "list", + "count": len(transforms), + "transforms": [transform.key for transform in transforms[:25]], + "explanation": step.explanation, + } + ) + continue + + if step.kind == "inspect": + if not step.transform_key: + raise ValueError("Could not identify transform to inspect") + transform = inspect_transform(step.transform_key) + outputs.append( + { + "kind": "inspect", + "transform": { + "key": transform.key, + "name": transform.name, + "category": transform.category, + "can_decode": transform.can_decode, + "options": [asdict(option) for option in transform.configurable_options], + }, + "explanation": step.explanation, + } + ) + continue + + if step.kind == "auto-decode": + source_text = step.text or current_text + if not source_text: + raise ValueError("No text available for auto-decode") + result = auto_decode(source_text) + outputs.append({"kind": "auto-decode", "result": result, "explanation": step.explanation}) + current_text = result["text"] if result else None + continue + + if step.kind == "run": + source_text = step.text if step.text is not None else current_text + if source_text is None: + raise ValueError(f"No text available for {step.action}") + result = run_transform(step.action or "encode", step.transform_key or "", source_text, step.options) + outputs.append( + { + "kind": "run", + "action": result.action, + "transform": result.transform_key, + "transform_name": result.transform_name, + "options": result.options, + "output": result.output, + "explanation": step.explanation, + } + ) + current_text = result.output + continue + + return { + "input": prompt, + "steps": [asdict(step) for step in steps], + "outputs": outputs, + "final_output": current_text, + } + diff --git a/p4rs3lt0ngv3_cli/bridge.py b/p4rs3lt0ngv3_cli/bridge.py new file mode 100644 index 0000000..7040b08 --- /dev/null +++ b/p4rs3lt0ngv3_cli/bridge.py @@ -0,0 +1,148 @@ +from __future__ import annotations + +import json +import subprocess +import sys +from functools import lru_cache +from pathlib import Path +from typing import Any + +from .models import TransformInfo, TransformOption, TransformResult + + +class BridgeError(RuntimeError): + """Raised when the Node bridge fails.""" + + +PROJECT_ROOT = Path(__file__).resolve().parent.parent +BRIDGE_PATH = PROJECT_ROOT / "scripts" / "cli_bridge.js" + + +def _run_bridge(payload: dict[str, Any]) -> dict[str, Any]: + process = subprocess.run( + ["node", str(BRIDGE_PATH)], + input=json.dumps(payload), + text=True, + capture_output=True, + cwd=PROJECT_ROOT, + check=False, + ) + if process.returncode != 0: + message = process.stderr.strip() or process.stdout.strip() or "Node bridge failed" + try: + parsed = json.loads(process.stdout) + message = parsed.get("error", message) + except json.JSONDecodeError: + pass + raise BridgeError(message) + + lines = [line for line in process.stdout.splitlines() if line.strip()] + if not lines: + raise BridgeError("Node bridge returned no output") + try: + data = json.loads(lines[-1]) + except json.JSONDecodeError as exc: + raise BridgeError(f"Node bridge returned invalid JSON: {lines[-1]}") from exc + if not data.get("ok"): + raise BridgeError(data.get("error", "Unknown bridge error")) + return data + + +@lru_cache(maxsize=1) +def list_transforms() -> list[TransformInfo]: + data = _run_bridge({"command": "list"}) + transforms = [] + for item in data["transforms"]: + transforms.append( + TransformInfo( + key=item["key"], + name=item["name"], + category=item["category"], + priority=item["priority"], + can_decode=item["canDecode"], + description=item.get("description", ""), + input_kind=item.get("inputKind", "textarea"), + configurable_options=[ + TransformOption( + id=opt["id"], + label=opt["label"], + type=opt["type"], + default=opt.get("default"), + min=opt.get("min"), + max=opt.get("max"), + step=opt.get("step"), + options=opt.get("options"), + ) + for opt in item.get("configurableOptions", []) + ], + ) + ) + return transforms + + +@lru_cache(maxsize=256) +def inspect_transform(transform_key: str) -> TransformInfo: + data = _run_bridge({"command": "inspect", "transform": transform_key}) + item = data["transform"] + return TransformInfo( + key=item["key"], + name=item["name"], + category=item["category"], + priority=item["priority"], + can_decode=item["canDecode"], + description=item.get("description", ""), + input_kind=item.get("inputKind", "textarea"), + configurable_options=[ + TransformOption( + id=opt["id"], + label=opt["label"], + type=opt["type"], + default=opt.get("default"), + min=opt.get("min"), + max=opt.get("max"), + step=opt.get("step"), + options=opt.get("options"), + ) + for opt in item.get("configurableOptions", []) + ], + ) + + +def run_transform(action: str, transform_key: str, text: str, options: dict[str, Any] | None = None) -> TransformResult: + data = _run_bridge( + { + "command": "run", + "action": action, + "transform": transform_key, + "text": text, + "options": options or {}, + } + ) + return TransformResult( + action=data["action"], + transform_key=data["transform"], + transform_name=data["name"], + options=data["options"], + output=data["output"], + ) + + +def auto_decode(text: str) -> dict[str, Any] | None: + data = _run_bridge({"command": "auto-decode", "text": text}) + return data["result"] + + +def ensure_node_available() -> None: + process = subprocess.run(["node", "--version"], capture_output=True, text=True, check=False) + if process.returncode != 0: + raise BridgeError("Node.js is required to run the P4RS3LT0NGV3 CLI") + + +def main_check() -> int: + try: + ensure_node_available() + except BridgeError as exc: + print(str(exc), file=sys.stderr) + return 1 + return 0 + diff --git a/p4rs3lt0ngv3_cli/cli.py b/p4rs3lt0ngv3_cli/cli.py new file mode 100644 index 0000000..d4d3d7d --- /dev/null +++ b/p4rs3lt0ngv3_cli/cli.py @@ -0,0 +1,183 @@ +from __future__ import annotations + +import argparse +import json +import sys +from typing import Any + +from .agent import execute_plan, find_transform +from .bridge import BridgeError, auto_decode, ensure_node_available, inspect_transform, list_transforms, run_transform + + +def parse_option_pairs(pairs: list[str]) -> dict[str, Any]: + options: dict[str, Any] = {} + for pair in pairs: + if "=" not in pair: + raise ValueError(f"Invalid option '{pair}'. Expected key=value") + key, value = pair.split("=", 1) + normalized = value.strip() + lowered = normalized.lower() + if lowered in {"true", "false"}: + coerced: Any = lowered == "true" + else: + try: + coerced = int(normalized) + except ValueError: + try: + coerced = float(normalized) + except ValueError: + coerced = normalized + options[key.strip()] = coerced + return options + + +def read_text_argument(value: str | None) -> str: + if value is not None: + return value + if not sys.stdin.isatty(): + return sys.stdin.read() + return "" + + +def emit(data: Any, as_json: bool) -> None: + if as_json: + print(json.dumps(data, indent=2, ensure_ascii=False)) + elif isinstance(data, str): + print(data) + else: + print(json.dumps(data, indent=2, ensure_ascii=False)) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(prog="p4rs3lt0ngv3-cli", description="Agent-based CLI for P4RS3LT0NGV3") + subparsers = parser.add_subparsers(dest="command", required=True) + + list_parser = subparsers.add_parser("list", help="List available transforms") + list_parser.add_argument("--category") + list_parser.add_argument("--json", action="store_true") + + inspect_parser = subparsers.add_parser("inspect", help="Inspect a transform") + inspect_parser.add_argument("transform") + inspect_parser.add_argument("--json", action="store_true") + + for command_name, action in [("encode", "encode"), ("decode", "decode"), ("preview", "preview")]: + cmd = subparsers.add_parser(command_name, help=f"{command_name.title()} text with a specific transform") + cmd.add_argument("--transform", required=True) + cmd.add_argument("--text") + cmd.add_argument("--option", action="append", default=[], help="Transform option in key=value form") + cmd.add_argument("--json", action="store_true") + cmd.set_defaults(action=action) + + autod = subparsers.add_parser("auto-decode", help="Use the universal decoder") + autod.add_argument("--text") + autod.add_argument("--json", action="store_true") + + agent = subparsers.add_parser("agent", help="Resolve a natural-language request") + agent.add_argument("prompt") + agent.add_argument("--json", action="store_true") + + return parser + + +def main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + + try: + ensure_node_available() + + if args.command == "list": + transforms = list_transforms() + if args.category: + transforms = [transform for transform in transforms if transform.category == args.category] + if args.json: + emit( + [ + { + "key": transform.key, + "name": transform.name, + "category": transform.category, + "can_decode": transform.can_decode, + } + for transform in transforms + ], + True, + ) + else: + for transform in transforms: + decode_flag = "decode" if transform.can_decode else "encode-only" + print(f"{transform.key:24} {transform.category:12} {decode_flag:11} {transform.name}") + return 0 + + if args.command == "inspect": + transform = find_transform(args.transform) + if not transform: + raise BridgeError(f"Unknown transform: {args.transform}") + meta = inspect_transform(transform.key) + data = { + "key": meta.key, + "name": meta.name, + "category": meta.category, + "priority": meta.priority, + "can_decode": meta.can_decode, + "input_kind": meta.input_kind, + "options": [ + { + "id": option.id, + "label": option.label, + "type": option.type, + "default": option.default, + "min": option.min, + "max": option.max, + "step": option.step, + "options": option.options, + } + for option in meta.configurable_options + ], + } + emit(data, args.json) + return 0 + + if args.command in {"encode", "decode", "preview"}: + transform = find_transform(args.transform) + if not transform: + raise BridgeError(f"Unknown transform: {args.transform}") + text = read_text_argument(args.text) + options = parse_option_pairs(args.option) + result = run_transform(args.action, transform.key, text, options) + if args.json: + emit( + { + "action": result.action, + "transform": result.transform_key, + "transform_name": result.transform_name, + "options": result.options, + "output": result.output, + }, + True, + ) + else: + emit(result.output, False) + return 0 + + if args.command == "auto-decode": + text = read_text_argument(args.text) + result = auto_decode(text) + emit(result or {}, args.json) + return 0 if result else 1 + + if args.command == "agent": + result = execute_plan(args.prompt) + emit(result if args.json else (result.get("final_output") or result), args.json) + return 0 + + except (BridgeError, ValueError) as exc: + print(str(exc), file=sys.stderr) + return 1 + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/p4rs3lt0ngv3_cli/models.py b/p4rs3lt0ngv3_cli/models.py new file mode 100644 index 0000000..ee16e26 --- /dev/null +++ b/p4rs3lt0ngv3_cli/models.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any + + +@dataclass(slots=True) +class TransformOption: + id: str + label: str + type: str + default: Any = None + min: float | int | None = None + max: float | int | None = None + step: float | int | None = None + options: list[dict[str, Any]] | None = None + + +@dataclass(slots=True) +class TransformInfo: + key: str + name: str + category: str + priority: int + can_decode: bool + description: str = "" + input_kind: str = "textarea" + configurable_options: list[TransformOption] = field(default_factory=list) + + @property + def search_tokens(self) -> set[str]: + tokens = {self.key.lower(), self.name.lower(), self.name.lower().replace(" ", "_")} + tokens.update(part for part in self.key.lower().split("_") if part) + tokens.update(part for part in self.name.lower().replace("-", " ").split() if part) + return tokens + + +@dataclass(slots=True) +class TransformResult: + action: str + transform_key: str + transform_name: str + options: dict[str, Any] + output: str + + +@dataclass(slots=True) +class AgentStep: + kind: str + transform_key: str | None = None + action: str | None = None + text: str | None = None + options: dict[str, Any] = field(default_factory=dict) + explanation: str = "" + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a43aec8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,27 @@ +[build-system] +requires = ["hatchling>=1.27.0"] +build-backend = "hatchling.build" + +[project] +name = "p4rs3lt0ngv3-cli" +version = "0.1.0" +description = "Agent-based CLI for the P4RS3LT0NGV3 transform catalog" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [] + +[project.scripts] +p4rs3lt0ngv3-cli = "p4rs3lt0ngv3_cli.cli:main" + +[dependency-groups] +dev = [ + "pytest>=8.3.5", +] + +[tool.hatch.build.targets.wheel] +packages = ["p4rs3lt0ngv3_cli"] + +[tool.pytest.ini_options] +testpaths = ["python_tests"] +addopts = "-q" + diff --git a/python_tests/__pycache__/test_cli_e2e.cpython-314-pytest-9.0.2.pyc b/python_tests/__pycache__/test_cli_e2e.cpython-314-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d61fa1e922326e74e38fd618e0b019c21a400c0a GIT binary patch literal 19050 zcmeHPTWlLwdY&OiB*l?P-E1qdV^NM}nT~a_F1GB%%4VHJO0toaiI+}NIV>$w5;rtS zof+AYcC%2M6!q>Vh`j)r0PetD(Q?|6zEYOD*MSwoUb&bWkFa7>= z=0=K+Y_FWU*$l{k{&Sr(=bZWPGfze8!x9}&{Bw3dm840Yuz+8j%jli2YzFlhu-oo)6+BV?ZIo_)60N%>+$28xQQv9(mXozvyR8#qU z!AKd|LS7$3S{Y9nGiErOFU}cBBlE5?NjGmzfD{0ZNtSQz1+pk5q_=%TA4pOfdAp?7 zxr2(e>^Vg>%JR#>k|{MaJ);{+X*Z;J{RYol6-y4QY=#)bo8k!}Ok1TJM?88a?9! z+Uxz_JJmZjlIZU#&Kom@Jnlj_^d5rA$y_#>>C1E%=gqog(z^9bGRY$74ZBIw_W&$P zzm`IB;3k1f{a?~)>1BReKK2&Ry@gut)}x`+;vb}$c^Sx}Gyy1%ak~GCqh0EjH07k6 zkWb1JG#~s(V7rR>J3{k}1M^p7`sU7`dJ$*;Xl2aEWU7dn%OneP2A0dL znViW?z7zMeMzl5kY%aghP#j=-f6nO5PoMAa!tKZXX4su)wu5TcjcX@ge&NKaq;~S; zsko1?5W~$#I*onr0aj17g6NMXkUfqRpQ{5v7NyVCZ66Q+bl42_UKv;p^?qL0x*lm= zuWwx6v2#7r{NMG#y167=}B(U7}EI+%WK?Q!$3i8pM!nkNO<5q$9RFvPAX4@0K zp;1Yi?eDaTrvH=n|Ae#B?;|&#=NZP$=H3vO~S|FkH$YuT4Of=gSJY%GNQE}i(HFmG|85Aw}v|N;* zmRa8fZ76<>x7wI{=HsMCqvEi8!Y&Roq7t-NVKn>k&P3kj+xm=o8O-|~hLfw{cESb>?%cGb+g#at@S z*^a6FKriIYnp^=yoVNryiwDFEsWdFn?;+EZ81N?gMM8|?us1QJ(@xtLqeD=!M%=BQ zNzx@PvdQSV;2fe2B z*{K_z+N;!`QwjYnjr{f4Cu2^I7r|veSEC;v`{}Vu(|?)&lYB|-SXMe*qGdDWdnTb* z1qo$DSJM0>MuIZuAZpCX=fBoaiwS##k_NUmV3Ee6 zG=VzeaEVkAjIcrazHiRgE6w>{L{GDOG~Yqb!}N~t74V>1X}47L7kv}Bg4KsZfQri7 ze=&P1Q8V^^u?jZgUaRU@6VB{9V-#$}ZC7=SZaa)E zpanO(r-Wk9g%;Yhu0x`Glrl0O)M~YGB3MEELmuMqj=DPW580!z&mjIPh`$2jpV8`O zf1dCYshEA|1M2DA-#qr~k~ew`w-M&suvFT2QNh2y5%N`?v4L5)s^4 zs=l^PJFIPekFRVE!dgU&Cc+hKpx(zCsP?GrvseT6=UT(J^DjvdGXAgTUwsm^xefoK zFDu2jnMy9J8+QU-U7UOS-^t9g-FP}oe=d_J;pGsd_0&8ubxQRUIJ^+?2suMS$Qc?l z17}lu=J0^2pcB2U4#XH1qh14(sf?atamYi%$K&!sFNs0@wmbyHm526I(ILn`kcNhZ zG;|m#+4N=eXxU^H$Fg}YL7{D2n#~zN6=6w9z>#yeF%|eFBj`* zIfPgP*{Yyt^kfb?cLtK4k;)aO+3RS_Ir>F#W@v4ZZI`Al&y*saizA;$+LnT)$o|EV zU+#UXq#pcaqV8T(tlz4#`{FZB z%0WKtchGCfZcoa|Mo$+nI4M-JQ#afm(fMV!Bd0PD)L&PdmzABDj1_g~nqvJ*%Fc^J zB6ZzGo#Go*-$`97OF7vWc}jUO#h5*f_L~ZFdr?Uz z36}&pMSe-X$>FCh;=ECob5!9wToo>t8iMwy!GlY-~@`f)?@2Sk-p! zo!-~Fl^Yal?qh`Nt@THP!h^sx8=n?VDqV9>B-fI(X#Ln=Sd+Rzhw5&+f?heuo13^Xs}(Jp%~ z=f^BXos_fec*y0^>@_MsOyE@jFNYmuCn=jAKb^K_Ig9PQ$4O*qG~G*mtO5uwn_X(X zGE|BTc=;@B43`E!8iq=L`3(+M)Q&a9`jwQ9i^C#y-9=Zau2aX0TvIxJ85#Zb^k;A0 z7#t}@MwgY*5BsrPC6&+{>gbwM_A61jlXuNc-B3o+Oq8qw-_R08zl90n-k24MS&r2q zDEHr#0Im%1(?(83mT5d+@~s2lS(R1?;fffrtOZ3fBwBU|v~rHGH0dgKwYzYq7H-nr zR%X%#-a}ik%cbqUa2}SaSWagB^J84+s>ic-YdCQ7E2f3tiGf3 zLl@tyuES+Fm~`Ei4sR+SRW(j!(_pQZ9&7u6R=?4tyUi~3jL|XA+GSi{sz%{StLG+N z?1(C++}Xx)kL}6sZMNTCw_*VHfb)qUmuN29D>8lSo+O%t1bz6c?Vj_Z<|r_oaQd>_XT*e1{pxDxx zA$GdJa%mx`2SuB-5Y}U;%%Dz6Y!ATLLc{AL3H|KI&~&2j==|A{*N(H(R7eUDsY*xh zL_Mm(Az0{41%|pqW+0uRk(fboPIE1Qb)Xqkf!`-{9rP;U_yl6<5mi)+ySPW;iiATk z{9ZRgNV3K7d;7^SXeky)tPDb0(n&FmNmiIc zf(FJepf#7P--rAww33;n`YZ2#*7^^}OOe+WM;;ZQC&uyd@K2x@9a&S`R@62i%W7Lm zZ6~DTh=409T&>oxL>1bdq=S(E)%smij@YRiK%rSt!7A_BgIk?YeuvxG=>-T{{~N5P z_RJmmr{RAbUORDm<;3aIi8o7;w;sW1?6ap4K5H?JeM@J^rF~{awfx!eI&+sc?%&N3 zxb4J7s@$bblSq}}(MG=>-0Flp-DgDhMu^KlbS09^?2Zzt%0o9LfKL^E`U)lT1Be{e zl}L(b8wjFT9ABFv8H5V>a9` zBXEuY*X+O9igSng@10)^pcm zq|Egg@X!y3H)GV+fdCDMmz9|?s@HXB*ap`lIP8|Y9>d^P-1P{zqgpXbb>GesEg?*M z&MawRP~zxa9WWQZ0NeWYgyU96yjh--M^+-zml?lS=}X)*FE?u)plxm5tKZ1a`rOK zwN1|aa_>zE;M3>%X(Ksvyi*HR6bm;d_96JCqTDalQ0A8c`Fupx35vAE42fgBx^>vK z_Ie{as@HW$@D}@}0%ht%aLd&RiVgW~{Zh3LtWHF^U#hXpFZE5?Gn=cR4lCBwP4}X! zPSkB$_wMQh;$gO2ov?P0N1dQJmL{_%g@6!ap;ed(W1XB^b;eS9OvGMpqBpQVz_WQ@ zsseitEvk$C2e{bJ2ZZRvJQ*sDOj6I%8-Gd5j>$5!b9h;W{RF*mntQ|+{Vz=~w;e4- zjy;0bwjSHs)?-`SdY0Z=RsdGio;Ah#l@t`iQ0C?coUCnD-ZiDiPTc?sYa11;QbSnV z9Ng*z`Acs(Z7UF5{-HC)aawAZDIO+nc=)ru{PZnT#N^-&+4~RH_?9VR-mHMhZ=|Tui)n5aUso zvB6EaLbWD2W_@FXtT}1<&ME+h2V-hxTaXY5^{)fx;X+TR%zv3QwHtS4WMA0RIo}7!8je< z>V)z;DtmMK!T=sR=S06_mg#O{}R+moHlY!GNN+pm;t!aPxSy{`N{)ZeHyuK ztf|a4=2gt9+q)da3?Y)P!dJbnBam;fjlsvU<+ia}k8P|hB)@tfe$oH7Z}$B}hz9`v zoJ==#O#k~m1pp!(4FQ17r-wWNfC%LOe-g$|FC1*Pc8=0>KkHl;5LkUrIIn0{y!;u~ zqFO_h7Z2%);fH6$b#|V6hSk;yKJx_m(Vn2U{}JTJE^D<=q*?^e=yKGktVf5688exD z%wi{;dsw;^y*6OhO~H+mv)?)YA+o9)3xncg9=hN)&rv<@$)OmVvUlSUyCPgRPf{Ij zPPE>Os}dW7?*#Do;zZcJ9sYvv4BT5>Js(CzT)pSrx4W7pc`S@K%H=Q*f)U{`P<*{@SFO@M-rxC@V4nz=^L|j^M3=+bxI+ XxxEVxsoN2lac|c_ZT)qf&3gYA3QB8X literal 0 HcmV?d00001 diff --git a/python_tests/test_cli_e2e.py b/python_tests/test_cli_e2e.py new file mode 100644 index 0000000..af9cbc4 --- /dev/null +++ b/python_tests/test_cli_e2e.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +import json +import subprocess +from pathlib import Path + + +PROJECT_ROOT = Path(__file__).resolve().parent.parent + + +def run_cli(*args: str, input_text: str | None = None) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["uv", "run", "p4rs3lt0ngv3-cli", *args], + cwd=PROJECT_ROOT, + input=input_text, + text=True, + capture_output=True, + check=False, + ) + + +def parse_json_output(process: subprocess.CompletedProcess[str]): + assert process.returncode == 0, process.stderr + return json.loads(process.stdout) + + +def test_list_json_exposes_large_catalog() -> None: + process = run_cli("list", "--json") + payload = parse_json_output(process) + keys = {item["key"] for item in payload} + assert len(payload) >= 150 + assert "base64" in keys + assert "caesar" in keys + + +def test_inspect_includes_transform_options() -> None: + process = run_cli("inspect", "caesar", "--json") + payload = parse_json_output(process) + assert payload["key"] == "caesar" + assert any(option["id"] == "shift" for option in payload["options"]) + + +def test_encode_and_decode_round_trip() -> None: + encoded = run_cli("encode", "--transform", "base64", "--text", "Hello World") + assert encoded.returncode == 0, encoded.stderr + assert encoded.stdout.strip() == "SGVsbG8gV29ybGQ=" + + decoded = run_cli("decode", "--transform", "base64", "--text", encoded.stdout.strip()) + assert decoded.returncode == 0, decoded.stderr + assert decoded.stdout.strip() == "Hello World" + + +def test_encode_supports_transform_options() -> None: + process = run_cli( + "encode", + "--transform", + "binary", + "--text", + "Hi", + "--option", + "byteSpacing=false", + ) + assert process.returncode == 0, process.stderr + assert process.stdout.strip() == "0100100001101001" + + +def test_auto_decode_uses_universal_decoder() -> None: + process = run_cli("auto-decode", "--text", "SGVsbG8=", "--json") + payload = parse_json_output(process) + assert payload["text"] == "Hello" + assert payload["method"] == "Base64" + + +def test_agent_can_route_simple_encode_request() -> None: + process = run_cli("agent", "encode 'Hello' as base64") + assert process.returncode == 0, process.stderr + assert process.stdout.strip() == "SGVsbG8=" + + +def test_agent_can_route_decode_request_with_options() -> None: + process = run_cli("agent", "decode 'Fyyfhp fy ifbs' from caesar shift 5") + assert process.returncode == 0, process.stderr + assert process.stdout.strip() == "Attack at dawn" + + +def test_agent_can_chain_steps() -> None: + process = run_cli("agent", "encode 'Hi' as base64 then decode from base64", "--json") + payload = parse_json_output(process) + assert payload["final_output"] == "Hi" + assert len(payload["outputs"]) == 2 diff --git a/scripts/cli_bridge.js b/scripts/cli_bridge.js new file mode 100644 index 0000000..8e909f1 --- /dev/null +++ b/scripts/cli_bridge.js @@ -0,0 +1,217 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); +const vm = require('vm'); + +const projectRoot = path.join(__dirname, '..'); +const transformsRoot = path.join(projectRoot, 'src', 'transformers'); +const transforms = require(path.join(projectRoot, 'src', 'transformers', 'loader-node.js')); + +function buildRegistry() { + const skipFiles = new Set(['BaseTransformer.js', 'index.js', 'loader-node.js', 'README.md']); + const categories = fs.readdirSync(transformsRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); + + const registry = []; + for (const category of categories) { + const categoryPath = path.join(transformsRoot, category); + const files = fs.readdirSync(categoryPath) + .filter((file) => file.endsWith('.js') && !skipFiles.has(file)) + .sort(); + + for (const file of files) { + const key = file.replace('.js', '').replace(/-/g, '_'); + const transform = transforms[key]; + if (!transform) { + continue; + } + + registry.push({ + key, + category, + name: transform.name, + priority: transform.priority ?? 0, + canDecode: Boolean(transform.reverse), + description: transform.description || '', + inputKind: transform.inputKind || 'textarea', + configurableOptions: (transform.configurableOptions || []).map((opt) => ({ + id: opt.id, + label: opt.label, + type: opt.type, + default: opt.default, + min: opt.min, + max: opt.max, + step: opt.step, + options: opt.options || undefined + })) + }); + } + } + + return registry; +} + +function getDefaultOptions(transform) { + if (!transform || !transform.configurableOptions || !transform.configurableOptions.length) { + return {}; + } + + const defaults = {}; + for (const opt of transform.configurableOptions) { + let value = opt.default; + if (value === undefined || value === null) { + if (opt.type === 'boolean') { + value = false; + } else if (opt.type === 'select' && opt.options && opt.options.length) { + value = opt.options[0].value; + } else if (opt.type === 'number') { + value = 0; + } else { + value = ''; + } + } + defaults[opt.id] = value; + } + + return defaults; +} + +function loadUniversalDecoder() { + const transformOptionsCode = fs.readFileSync(path.join(projectRoot, 'js', 'core', 'transformOptions.js'), 'utf8'); + const decoderCode = fs.readFileSync(path.join(projectRoot, 'js', 'core', 'decoder.js'), 'utf8'); + const emojiWordMapCode = fs.readFileSync(path.join(projectRoot, 'src', 'emojiWordMap.js'), 'utf8'); + const emojiUtilsCode = fs.readFileSync(path.join(projectRoot, 'js', 'utils', 'emoji.js'), 'utf8'); + + const mockSteganography = { + hasEmojiInText: () => false, + decodeEmoji: () => null, + decodeInvisible: () => null + }; + + const sandbox = { + window: { + transforms, + steganography: mockSteganography, + emojiLibrary: {}, + emojiKeywords: {}, + emojiData: {} + }, + console, + TextEncoder, + TextDecoder, + Intl, + btoa: (str) => Buffer.from(str, 'binary').toString('base64'), + atob: (str) => Buffer.from(str, 'base64').toString('binary') + }; + + vm.createContext(sandbox); + vm.runInContext(emojiUtilsCode, sandbox); + vm.runInContext(emojiWordMapCode, sandbox); + vm.runInContext(transformOptionsCode, sandbox); + vm.runInContext(decoderCode, sandbox); + + return sandbox.universalDecode; +} + +function normalizeError(error) { + if (error && typeof error.message === 'string') { + return error.message; + } + return String(error); +} + +function readPayload() { + const raw = fs.readFileSync(0, 'utf8'); + return raw ? JSON.parse(raw) : {}; +} + +function writeResult(result, exitCode = 0) { + process.stdout.write(`${JSON.stringify(result)}\n`); + process.exit(exitCode); +} + +function main() { + const payload = readPayload(); + const command = payload.command; + const registry = buildRegistry(); + + if (command === 'list') { + writeResult({ ok: true, transforms: registry }); + } + + const transformKey = payload.transform; + const transform = transformKey ? transforms[transformKey] : null; + + if (command === 'inspect') { + if (!transform || !transformKey) { + writeResult({ ok: false, error: `Unknown transform: ${transformKey}` }, 1); + } + + const meta = registry.find((entry) => entry.key === transformKey); + writeResult({ + ok: true, + transform: { + ...meta, + defaultOptions: getDefaultOptions(transform) + } + }); + } + + if (command === 'run') { + if (!transform || !transformKey) { + writeResult({ ok: false, error: `Unknown transform: ${transformKey}` }, 1); + } + + const action = payload.action || 'encode'; + const text = payload.text || ''; + const options = { + ...getDefaultOptions(transform), + ...(payload.options || {}) + }; + + try { + let output; + if (action === 'encode') { + output = transform.func(text, options); + } else if (action === 'decode') { + if (!transform.reverse) { + throw new Error(`${transform.name} does not support decode`); + } + output = transform.reverse(text, options); + } else if (action === 'preview') { + output = transform.preview(text, options); + } else { + throw new Error(`Unsupported action: ${action}`); + } + + writeResult({ + ok: true, + action, + transform: transformKey, + name: transform.name, + options, + output + }); + } catch (error) { + writeResult({ ok: false, error: normalizeError(error) }, 1); + } + } + + if (command === 'auto-decode') { + const decode = loadUniversalDecoder(); + try { + const result = decode(payload.text || '', {}); + writeResult({ ok: true, result }); + } catch (error) { + writeResult({ ok: false, error: normalizeError(error) }, 1); + } + } + + writeResult({ ok: false, error: `Unknown command: ${command}` }, 1); +} + +main(); + diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..8763594 --- /dev/null +++ b/uv.lock @@ -0,0 +1,79 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "p4rs3lt0ngv3-cli" +version = "0.1.0" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8.3.5" }] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +]