mirror of
https://github.com/elder-plinius/P4RS3LT0NGV3.git
synced 2026-06-06 06:53:56 +02:00
Merge pull request #19 from GangGreenTemperTatum/cli/P4RS3LT0NGV3
[feat] add uv-powered agent cli
This commit is contained in:
@@ -4,6 +4,9 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
"""Python CLI package for P4RS3LT0NGV3."""
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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();
|
||||
|
||||
@@ -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" },
|
||||
]
|
||||
Reference in New Issue
Block a user