Merge pull request #19 from GangGreenTemperTatum/cli/P4RS3LT0NGV3

[feat] add uv-powered agent cli
This commit is contained in:
pliny
2026-04-01 08:06:23 -07:00
committed by GitHub
11 changed files with 1295 additions and 0 deletions
+3
View File
@@ -4,6 +4,9 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
.venv/
__pycache__/
*.py[cod]
# Build outputs
dist/
+22
View File
@@ -314,6 +314,28 @@ 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"
uv run p4rs3lt0ngv3-cli /base64 Hello
uv run p4rs3lt0ngv3-cli /base64 --decode SGVsbG8=
uv run p4rs3lt0ngv3-cli /caesar --shift 5 "Attack at dawn"
```
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
+2
View File
@@ -0,0 +1,2 @@
"""Python CLI package for P4RS3LT0NGV3."""
+250
View File
@@ -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<text>[^'"]+)['"]""")
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,
}
+148
View File
@@ -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
+378
View File
@@ -0,0 +1,378 @@
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 coerce_option_value(raw: str) -> Any:
lowered = raw.lower()
if lowered in {"true", "false"}:
return lowered == "true"
try:
return int(raw)
except ValueError:
try:
return float(raw)
except ValueError:
return raw
def parse_slash_command(argv: list[str]) -> tuple[str, dict[str, Any]] | None:
if not argv:
return None
first = argv[0]
if not first.startswith("/") or first == "/":
return None
name = first[1:]
if not name:
return None
if name == "inspect":
if len(argv) < 2:
raise ValueError("Usage: /inspect <transform> [--json]")
return ("inspect", {"transform": argv[1], "json": "--json" in argv[2:]})
if name == "list":
category = None
json_mode = False
tokens = argv[1:]
index = 0
while index < len(tokens):
token = tokens[index]
if token == "--json":
json_mode = True
elif token == "--category":
if index + 1 >= len(tokens):
raise ValueError("Missing value for --category")
category = tokens[index + 1]
index += 1
else:
raise ValueError(f"Unsupported option for /list: {token}")
index += 1
return ("list", {"category": category, "json": json_mode})
if name == "decode":
tokens = argv[1:]
json_mode = False
if "--json" in tokens:
json_mode = True
tokens = [token for token in tokens if token != "--json"]
return ("auto-decode", {"text": " ".join(tokens), "json": json_mode})
action = "encode"
json_mode = False
options: dict[str, Any] = {}
text_tokens: list[str] = []
tokens = argv[1:]
index = 0
while index < len(tokens):
token = tokens[index]
if token == "--decode":
action = "decode"
elif token == "--preview":
action = "preview"
elif token == "--json":
json_mode = True
elif token == "--option":
if index + 1 >= len(tokens):
raise ValueError("Missing value for --option")
options.update(parse_option_pairs([tokens[index + 1]]))
index += 1
elif token.startswith("--"):
flag = token[2:]
if "=" in flag:
key, value = flag.split("=", 1)
options[key] = coerce_option_value(value)
elif index + 1 < len(tokens) and not tokens[index + 1].startswith("--"):
options[flag] = coerce_option_value(tokens[index + 1])
index += 1
else:
options[flag] = True
else:
text_tokens.append(token)
index += 1
return (
"transform-shortcut",
{
"transform": name,
"action": action,
"text": " ".join(text_tokens),
"json": json_mode,
"options": options,
},
)
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:
raw_argv = list(sys.argv[1:] if argv is None else argv)
try:
slash_command = parse_slash_command(raw_argv)
except ValueError as exc:
print(str(exc), file=sys.stderr)
return 1
if slash_command is not None:
command, payload = slash_command
try:
ensure_node_available()
if command == "list":
transforms = list_transforms()
if payload["category"]:
transforms = [transform for transform in transforms if transform.category == payload["category"]]
if payload["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 command == "inspect":
transform = find_transform(payload["transform"])
if not transform:
raise BridgeError(f"Unknown transform: {payload['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, payload["json"])
return 0
if command == "auto-decode":
result = auto_decode(payload["text"])
emit(result or {}, payload["json"])
return 0 if result else 1
if command == "transform-shortcut":
transform = find_transform(payload["transform"])
if not transform:
raise BridgeError(f"Unknown transform: {payload['transform']}")
text = payload["text"] or read_text_argument(None)
result = run_transform(payload["action"], transform.key, text, payload["options"])
if payload["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
except (BridgeError, ValueError) as exc:
print(str(exc), file=sys.stderr)
return 1
parser = build_parser()
args = parser.parse_args(raw_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())
+55
View File
@@ -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 = ""
+27
View File
@@ -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"
+114
View File
@@ -0,0 +1,114 @@
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
def test_slash_command_encodes_text() -> None:
process = run_cli("/base64", "Hello")
assert process.returncode == 0, process.stderr
assert process.stdout.strip() == "SGVsbG8="
def test_slash_command_decodes_text() -> None:
process = run_cli("/base64", "--decode", "SGVsbG8=")
assert process.returncode == 0, process.stderr
assert process.stdout.strip() == "Hello"
def test_slash_command_supports_transform_flags() -> None:
process = run_cli("/caesar", "--shift", "5", "Attack", "at", "dawn")
assert process.returncode == 0, process.stderr
assert process.stdout.strip() == "Fyyfhp fy ifbs"
def test_slash_command_supports_inspect() -> None:
process = run_cli("/inspect", "caesar", "--json")
payload = parse_json_output(process)
assert payload["key"] == "caesar"
+217
View File
@@ -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();
Generated
+79
View File
@@ -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" },
]