* fix(skill): enhance git-worktree-manager with scripts, references, and Anthropic best practices * fix(skill): enhance mcp-server-builder with scripts, references, and Anthropic best practices * fix(skill): enhance changelog-generator with scripts, references, and Anthropic best practices * fix(skill): enhance ci-cd-pipeline-builder with scripts, references, and Anthropic best practices * fix(skill): enhance prompt-engineer-toolkit with scripts, references, and Anthropic best practices * docs: update README, CHANGELOG, and plugin metadata * fix: correct marketing plugin count, expand thin references --------- Co-authored-by: Leo <leo@openclaw.ai>
236 lines
7.9 KiB
Python
Executable File
236 lines
7.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Version and diff prompts with a local JSONL history store.
|
|
|
|
Commands:
|
|
- add
|
|
- list
|
|
- diff
|
|
- changelog
|
|
|
|
Input modes:
|
|
- prompt text via --prompt, --prompt-file, --input JSON, or stdin JSON
|
|
"""
|
|
|
|
import argparse
|
|
import difflib
|
|
import json
|
|
import sys
|
|
from dataclasses import dataclass, asdict
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
|
|
class CLIError(Exception):
|
|
"""Raised for expected CLI failures."""
|
|
|
|
|
|
@dataclass
|
|
class PromptVersion:
|
|
name: str
|
|
version: int
|
|
author: str
|
|
timestamp: str
|
|
change_note: str
|
|
prompt: str
|
|
|
|
|
|
def add_common_subparser_args(parser: argparse.ArgumentParser) -> None:
|
|
parser.add_argument("--store", default=".prompt_versions.jsonl", help="JSONL history file path.")
|
|
parser.add_argument("--input", help="Optional JSON input file with prompt payload.")
|
|
parser.add_argument("--format", choices=["text", "json"], default="text", help="Output format.")
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(description="Version and diff prompts.")
|
|
|
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
|
|
add = sub.add_parser("add", help="Add a new prompt version.")
|
|
add_common_subparser_args(add)
|
|
add.add_argument("--name", required=True, help="Prompt identifier.")
|
|
add.add_argument("--prompt", help="Prompt text.")
|
|
add.add_argument("--prompt-file", help="Prompt file path.")
|
|
add.add_argument("--author", default="unknown", help="Author name.")
|
|
add.add_argument("--change-note", default="", help="Reason for this revision.")
|
|
|
|
ls = sub.add_parser("list", help="List versions for a prompt.")
|
|
add_common_subparser_args(ls)
|
|
ls.add_argument("--name", required=True, help="Prompt identifier.")
|
|
|
|
diff = sub.add_parser("diff", help="Diff two prompt versions.")
|
|
add_common_subparser_args(diff)
|
|
diff.add_argument("--name", required=True, help="Prompt identifier.")
|
|
diff.add_argument("--from-version", type=int, required=True)
|
|
diff.add_argument("--to-version", type=int, required=True)
|
|
|
|
changelog = sub.add_parser("changelog", help="Show changelog for a prompt.")
|
|
add_common_subparser_args(changelog)
|
|
changelog.add_argument("--name", required=True, help="Prompt identifier.")
|
|
return parser
|
|
|
|
|
|
def read_optional_json(input_path: Optional[str]) -> Dict[str, Any]:
|
|
if input_path:
|
|
try:
|
|
return json.loads(Path(input_path).read_text(encoding="utf-8"))
|
|
except Exception as exc:
|
|
raise CLIError(f"Failed reading --input: {exc}") from exc
|
|
|
|
if not sys.stdin.isatty():
|
|
raw = sys.stdin.read().strip()
|
|
if raw:
|
|
try:
|
|
return json.loads(raw)
|
|
except json.JSONDecodeError as exc:
|
|
raise CLIError(f"Invalid JSON from stdin: {exc}") from exc
|
|
|
|
return {}
|
|
|
|
|
|
def read_store(path: Path) -> List[PromptVersion]:
|
|
if not path.exists():
|
|
return []
|
|
versions: List[PromptVersion] = []
|
|
for line in path.read_text(encoding="utf-8").splitlines():
|
|
if not line.strip():
|
|
continue
|
|
obj = json.loads(line)
|
|
versions.append(PromptVersion(**obj))
|
|
return versions
|
|
|
|
|
|
def write_store(path: Path, versions: List[PromptVersion]) -> None:
|
|
payload = "\n".join(json.dumps(asdict(v), ensure_ascii=True) for v in versions)
|
|
path.write_text(payload + ("\n" if payload else ""), encoding="utf-8")
|
|
|
|
|
|
def get_prompt_text(args: argparse.Namespace, payload: Dict[str, Any]) -> str:
|
|
if args.prompt:
|
|
return args.prompt
|
|
if args.prompt_file:
|
|
try:
|
|
return Path(args.prompt_file).read_text(encoding="utf-8")
|
|
except Exception as exc:
|
|
raise CLIError(f"Failed reading prompt file: {exc}") from exc
|
|
if payload.get("prompt"):
|
|
return str(payload["prompt"])
|
|
raise CLIError("Prompt content required via --prompt, --prompt-file, --input JSON, or stdin JSON.")
|
|
|
|
|
|
def next_version(versions: List[PromptVersion], name: str) -> int:
|
|
existing = [v.version for v in versions if v.name == name]
|
|
return (max(existing) + 1) if existing else 1
|
|
|
|
|
|
def main() -> int:
|
|
parser = build_parser()
|
|
args = parser.parse_args()
|
|
payload = read_optional_json(args.input)
|
|
|
|
store_path = Path(args.store)
|
|
versions = read_store(store_path)
|
|
|
|
if args.command == "add":
|
|
prompt_name = str(payload.get("name", args.name))
|
|
prompt_text = get_prompt_text(args, payload)
|
|
author = str(payload.get("author", args.author))
|
|
change_note = str(payload.get("change_note", args.change_note))
|
|
|
|
item = PromptVersion(
|
|
name=prompt_name,
|
|
version=next_version(versions, prompt_name),
|
|
author=author,
|
|
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
change_note=change_note,
|
|
prompt=prompt_text,
|
|
)
|
|
versions.append(item)
|
|
write_store(store_path, versions)
|
|
output: Dict[str, Any] = {"added": asdict(item), "store": str(store_path.resolve())}
|
|
|
|
elif args.command == "list":
|
|
prompt_name = str(payload.get("name", args.name))
|
|
matches = [asdict(v) for v in versions if v.name == prompt_name]
|
|
output = {"name": prompt_name, "versions": matches}
|
|
|
|
elif args.command == "changelog":
|
|
prompt_name = str(payload.get("name", args.name))
|
|
matches = [v for v in versions if v.name == prompt_name]
|
|
entries = [
|
|
{
|
|
"version": v.version,
|
|
"author": v.author,
|
|
"timestamp": v.timestamp,
|
|
"change_note": v.change_note,
|
|
}
|
|
for v in matches
|
|
]
|
|
output = {"name": prompt_name, "changelog": entries}
|
|
|
|
elif args.command == "diff":
|
|
prompt_name = str(payload.get("name", args.name))
|
|
from_v = int(payload.get("from_version", args.from_version))
|
|
to_v = int(payload.get("to_version", args.to_version))
|
|
|
|
by_name = [v for v in versions if v.name == prompt_name]
|
|
old = next((v for v in by_name if v.version == from_v), None)
|
|
new = next((v for v in by_name if v.version == to_v), None)
|
|
if not old or not new:
|
|
raise CLIError("Requested versions not found for prompt name.")
|
|
|
|
diff_lines = list(
|
|
difflib.unified_diff(
|
|
old.prompt.splitlines(),
|
|
new.prompt.splitlines(),
|
|
fromfile=f"{prompt_name}@v{from_v}",
|
|
tofile=f"{prompt_name}@v{to_v}",
|
|
lineterm="",
|
|
)
|
|
)
|
|
output = {
|
|
"name": prompt_name,
|
|
"from_version": from_v,
|
|
"to_version": to_v,
|
|
"diff": diff_lines,
|
|
}
|
|
|
|
else:
|
|
raise CLIError("Unknown command.")
|
|
|
|
if args.format == "json":
|
|
print(json.dumps(output, indent=2))
|
|
else:
|
|
if args.command == "add":
|
|
added = output["added"]
|
|
print("Prompt version added")
|
|
print(f"- name: {added['name']}")
|
|
print(f"- version: {added['version']}")
|
|
print(f"- author: {added['author']}")
|
|
print(f"- store: {output['store']}")
|
|
elif args.command in ("list", "changelog"):
|
|
print(f"Prompt: {output['name']}")
|
|
key = "versions" if args.command == "list" else "changelog"
|
|
items = output[key]
|
|
if not items:
|
|
print("- no entries")
|
|
else:
|
|
for item in items:
|
|
line = f"- v{item.get('version')} by {item.get('author')} at {item.get('timestamp')}"
|
|
note = item.get("change_note")
|
|
if note:
|
|
line += f" | {note}"
|
|
print(line)
|
|
else:
|
|
print("\n".join(output["diff"]) if output["diff"] else "No differences.")
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
raise SystemExit(main())
|
|
except CLIError as exc:
|
|
print(f"ERROR: {exc}", file=sys.stderr)
|
|
raise SystemExit(2)
|