feat: 30 new variants — deep intel/military + professional specializations
Intel/Military Deep (18 variants):
frodo/pakistan, india, nato-alliance, nuclear, energy-geopolitics, turkey
marshal/russian-doctrine, chinese-doctrine, turkish-doctrine, iranian-military
warden/drone-warfare, naval-warfare, electronic-warfare
centurion/ukraine-russia, ottoman-wars
wraith/case-studies (Ames, Penkovsky, Cambridge Five)
echo/electronic-order-of-battle
ghost/russian-info-war (IRA, GRU cyber, dezinformatsiya)
scribe/cold-war-ops (CIA/KGB ops, VENONA, Gladio)
Professional Specializations (12 variants):
neo/social-engineering, mobile-security
phantom/bug-bounty
specter/firmware
bastion/incident-commander
sentinel/darknet
oracle/crypto-osint
marshal/wargaming
corsair/proxy-warfare
polyglot/swahili
forge/agent-dev
Dynamic config system:
config.yaml — user-specific settings
config.example.yaml — template for new users
build.py — config-aware with {{variable}} injection + conditionals
Total: 108 prompt files, 20,717 lines, 29 personas
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
170
build.py
170
build.py
@@ -1,8 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build script: Generate .yaml and .json from persona .md files."""
|
||||
"""Build script: Generate .yaml, .json, .prompt.md from persona .md files.
|
||||
|
||||
Supports config.yaml for dynamic variable injection and user-specific customization.
|
||||
New users: copy config.example.yaml → config.yaml and customize.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -14,10 +17,89 @@ except ImportError:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def parse_persona_md(filepath: Path) -> dict:
|
||||
def load_config(root: Path) -> dict:
|
||||
"""Load config.yaml if it exists, otherwise return empty config."""
|
||||
config_path = root / "config.yaml"
|
||||
if config_path.exists():
|
||||
config = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
|
||||
print(f"Config loaded: {config_path}")
|
||||
return config
|
||||
|
||||
example_path = root / "config.example.yaml"
|
||||
if example_path.exists():
|
||||
print("WARN: No config.yaml found. Using defaults. Copy config.example.yaml → config.yaml to customize.")
|
||||
return {}
|
||||
|
||||
|
||||
def flatten_config(config: dict, prefix: str = "") -> dict:
|
||||
"""Flatten nested config dict for template substitution.
|
||||
|
||||
Example: {"user": {"name": "Salva"}} → {"user.name": "Salva"}
|
||||
"""
|
||||
flat = {}
|
||||
for key, value in config.items():
|
||||
full_key = f"{prefix}{key}" if not prefix else f"{prefix}.{key}"
|
||||
if isinstance(value, dict):
|
||||
flat.update(flatten_config(value, full_key))
|
||||
elif isinstance(value, list):
|
||||
flat[full_key] = value
|
||||
flat[f"{full_key}.count"] = len(value)
|
||||
flat[f"{full_key}.csv"] = ", ".join(str(v) for v in value if not isinstance(v, dict))
|
||||
else:
|
||||
flat[full_key] = value
|
||||
return flat
|
||||
|
||||
|
||||
def inject_config(content: str, flat_config: dict) -> str:
|
||||
"""Replace {{config.key}} placeholders with config values."""
|
||||
def replacer(match):
|
||||
key = match.group(1).strip()
|
||||
value = flat_config.get(key, match.group(0)) # keep original if not found
|
||||
if isinstance(value, list):
|
||||
return ", ".join(str(v) for v in value if not isinstance(v, dict))
|
||||
if isinstance(value, bool):
|
||||
return "enabled" if value else "disabled"
|
||||
return str(value)
|
||||
|
||||
return re.sub(r"\{\{(.+?)\}\}", replacer, content)
|
||||
|
||||
|
||||
def check_conditionals(content: str, flat_config: dict) -> str:
|
||||
"""Process {{#if key}}...{{/if}} and {{#unless key}}...{{/unless}} blocks."""
|
||||
# Handle {{#if key}}content{{/if}}
|
||||
def if_replacer(match):
|
||||
key = match.group(1).strip()
|
||||
body = match.group(2)
|
||||
value = flat_config.get(key)
|
||||
if value and value not in (False, 0, "", "false", "none", "disabled", None, []):
|
||||
return body
|
||||
return ""
|
||||
|
||||
content = re.sub(r"\{\{#if (.+?)\}\}(.*?)\{\{/if\}\}", if_replacer, content, flags=re.DOTALL)
|
||||
|
||||
# Handle {{#unless key}}content{{/unless}}
|
||||
def unless_replacer(match):
|
||||
key = match.group(1).strip()
|
||||
body = match.group(2)
|
||||
value = flat_config.get(key)
|
||||
if not value or value in (False, 0, "", "false", "none", "disabled", None, []):
|
||||
return body
|
||||
return ""
|
||||
|
||||
content = re.sub(r"\{\{#unless (.+?)\}\}(.*?)\{\{/unless\}\}", unless_replacer, content, flags=re.DOTALL)
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def parse_persona_md(filepath: Path, flat_config: dict) -> dict:
|
||||
"""Parse a persona markdown file into structured data."""
|
||||
content = filepath.read_text(encoding="utf-8")
|
||||
|
||||
# Apply config injection
|
||||
if flat_config:
|
||||
content = check_conditionals(content, flat_config)
|
||||
content = inject_config(content, flat_config)
|
||||
|
||||
# Extract YAML frontmatter
|
||||
fm_match = re.match(r"^---\n(.*?)\n---\n(.*)$", content, re.DOTALL)
|
||||
if not fm_match:
|
||||
@@ -51,11 +133,11 @@ def parse_persona_md(filepath: Path) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def build_persona(persona_dir: Path, output_dir: Path):
|
||||
def build_persona(persona_dir: Path, output_dir: Path, flat_config: dict, config: dict):
|
||||
"""Build all variants for a persona directory."""
|
||||
md_files = sorted(persona_dir.glob("*.md"))
|
||||
if not md_files:
|
||||
return
|
||||
return 0
|
||||
|
||||
persona_name = persona_dir.name
|
||||
out_path = output_dir / persona_name
|
||||
@@ -65,20 +147,38 @@ def build_persona(persona_dir: Path, output_dir: Path):
|
||||
meta_file = persona_dir / "_meta.yaml"
|
||||
meta = {}
|
||||
if meta_file.exists():
|
||||
meta = yaml.safe_load(meta_file.read_text(encoding="utf-8")) or {}
|
||||
meta_content = meta_file.read_text(encoding="utf-8")
|
||||
if flat_config:
|
||||
meta_content = inject_config(meta_content, flat_config)
|
||||
meta = yaml.safe_load(meta_content) or {}
|
||||
|
||||
# Apply config overrides for address
|
||||
addresses = config.get("persona_defaults", {}).get("custom_addresses", {})
|
||||
if persona_name in addresses:
|
||||
meta["address_to"] = addresses[persona_name]
|
||||
|
||||
count = 0
|
||||
for md_file in md_files:
|
||||
if md_file.name.startswith("_"):
|
||||
continue
|
||||
|
||||
variant = md_file.stem
|
||||
parsed = parse_persona_md(md_file)
|
||||
parsed = parse_persona_md(md_file, flat_config)
|
||||
if not parsed:
|
||||
continue
|
||||
|
||||
# Merge meta into parsed data
|
||||
# Build output object
|
||||
output = {**meta, **parsed["metadata"], "variant": variant, "sections": parsed["sections"]}
|
||||
|
||||
# Inject config metadata
|
||||
if config:
|
||||
output["_config"] = {
|
||||
"user": config.get("user", {}).get("name", "unknown"),
|
||||
"tools": {k: v for k, v in config.get("infrastructure", {}).get("tools", {}).items() if v is True},
|
||||
"frameworks": {k: v for k, v in config.get("frameworks", {}).items() if v is True},
|
||||
"regional_focus": config.get("regional_focus", {}),
|
||||
}
|
||||
|
||||
# Write YAML
|
||||
yaml_out = out_path / f"{variant}.yaml"
|
||||
yaml_out.write_text(
|
||||
@@ -90,16 +190,23 @@ def build_persona(persona_dir: Path, output_dir: Path):
|
||||
json_out = out_path / f"{variant}.json"
|
||||
json_out.write_text(json.dumps(output, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
# Write plain system prompt (just the body)
|
||||
# Write plain system prompt (just the body, no config metadata)
|
||||
prompt_out = out_path / f"{variant}.prompt.md"
|
||||
prompt_out.write_text(parsed["raw_body"], encoding="utf-8")
|
||||
|
||||
count += 1
|
||||
print(f" Built: {persona_name}/{variant} -> .yaml .json .prompt.md")
|
||||
|
||||
return count
|
||||
|
||||
def build_catalog(personas_dir: Path, output_dir: Path):
|
||||
|
||||
def build_catalog(personas_dir: Path, output_dir: Path, config: dict):
|
||||
"""Generate CATALOG.md from all personas."""
|
||||
catalog_lines = ["# Persona Catalog\n", "_Auto-generated by build.py_\n"]
|
||||
addresses = config.get("persona_defaults", {}).get("custom_addresses", {})
|
||||
catalog_lines = [
|
||||
"# Persona Catalog\n",
|
||||
f"_Auto-generated by build.py | User: {config.get('user', {}).get('name', 'default')}_\n",
|
||||
]
|
||||
|
||||
for persona_dir in sorted(personas_dir.iterdir()):
|
||||
if not persona_dir.is_dir() or persona_dir.name.startswith((".", "_")):
|
||||
@@ -110,11 +217,13 @@ def build_catalog(personas_dir: Path, output_dir: Path):
|
||||
continue
|
||||
|
||||
meta = yaml.safe_load(meta_file.read_text(encoding="utf-8")) or {}
|
||||
codename = meta.get("codename", persona_dir.name)
|
||||
address = addresses.get(persona_dir.name, meta.get("address_to", "N/A"))
|
||||
variants = [f.stem for f in sorted(persona_dir.glob("*.md")) if not f.name.startswith("_")]
|
||||
|
||||
catalog_lines.append(f"## {meta.get('codename', persona_dir.name)} — {meta.get('role', 'Unknown')}")
|
||||
catalog_lines.append(f"## {codename} — {meta.get('role', 'Unknown')}")
|
||||
catalog_lines.append(f"- **Domain:** {meta.get('domain', 'N/A')}")
|
||||
catalog_lines.append(f"- **Hitap:** {meta.get('address_to', 'N/A')}")
|
||||
catalog_lines.append(f"- **Hitap:** {address}")
|
||||
catalog_lines.append(f"- **Variants:** {', '.join(variants)}")
|
||||
catalog_lines.append("")
|
||||
|
||||
@@ -123,6 +232,30 @@ def build_catalog(personas_dir: Path, output_dir: Path):
|
||||
print(f" Catalog: {catalog_path}")
|
||||
|
||||
|
||||
def print_summary(config: dict, total_personas: int, total_variants: int):
|
||||
"""Print build summary with config status."""
|
||||
print("\n" + "=" * 50)
|
||||
print(f"BUILD COMPLETE")
|
||||
print(f" Personas: {total_personas}")
|
||||
print(f" Variants: {total_variants}")
|
||||
print(f" Output: .generated/")
|
||||
|
||||
if config:
|
||||
user = config.get("user", {}).get("name", "?")
|
||||
tools_on = sum(1 for v in config.get("infrastructure", {}).get("tools", {}).values() if v is True)
|
||||
frameworks_on = sum(1 for v in config.get("frameworks", {}).values() if v is True)
|
||||
regions = config.get("regional_focus", {}).get("primary", [])
|
||||
print(f"\n Config: {user}")
|
||||
print(f" Tools: {tools_on} enabled")
|
||||
print(f" Frameworks: {frameworks_on} enabled")
|
||||
if regions:
|
||||
print(f" Regions: {', '.join(regions)}")
|
||||
else:
|
||||
print("\n Config: none (using defaults)")
|
||||
print(" Tip: Copy config.example.yaml → config.yaml to customize")
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
def main():
|
||||
root = Path(__file__).parent
|
||||
personas_dir = root / "personas"
|
||||
@@ -133,6 +266,10 @@ def main():
|
||||
|
||||
output_dir = root / ".generated"
|
||||
|
||||
# Load config
|
||||
config = load_config(root)
|
||||
flat_config = flatten_config(config) if config else {}
|
||||
|
||||
# Find all persona directories
|
||||
persona_dirs = [
|
||||
d for d in sorted(personas_dir.iterdir()) if d.is_dir() and not d.name.startswith((".", "_"))
|
||||
@@ -145,11 +282,12 @@ def main():
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
print(f"Building {len(persona_dirs)} personas -> {output_dir}\n")
|
||||
|
||||
total_variants = 0
|
||||
for pdir in persona_dirs:
|
||||
build_persona(pdir, output_dir)
|
||||
total_variants += build_persona(pdir, output_dir, flat_config, config)
|
||||
|
||||
build_catalog(personas_dir, output_dir)
|
||||
print(f"\nDone. {len(persona_dirs)} personas built.")
|
||||
build_catalog(personas_dir, output_dir, config)
|
||||
print_summary(config, len(persona_dirs), total_variants)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user