- onboard-client.py: client replica scaffolding CLI - gws/: Google Workspace sync (Gmail, Calendar, Drive) - lifestream/: life event stream collector - muscriptor-mcp/: audio → MIDI MCP server - music-mcp/: music library MCP server - data_sharing/: consent-gated data sharing (Python + TS) - sync-mirrors.py: GitHub → Forgejo mirror engine - brain-to-gbrain.py, vault-sync.py, test-all.sh - shared/: TS data-sharing library + index - dirac: provider registry update - .gitignore: exclude Rust build artifacts Co-authored-by: Álvaro de Campos <campos@portugalfuturista.org>
424 lines
17 KiB
Python
424 lines
17 KiB
Python
"""
|
|
Scaffolder — creates the full directory tree and files for a new client.
|
|
|
|
This is the engine that turns an OnboardingConfig into a living replica-omnisciente
|
|
skeleton on disk.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import stat
|
|
from pathlib import Path
|
|
|
|
from .config import OnboardingConfig
|
|
from . import templates
|
|
|
|
|
|
class ScaffoldError(Exception):
|
|
pass
|
|
|
|
|
|
def _write(path: Path, content: str, cfg: OnboardingConfig, executable: bool = False) -> bool:
|
|
"""Write a file unless dry_run. Returns True if written."""
|
|
if cfg.dry_run:
|
|
print(f" [DRY] {path}")
|
|
return False
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(content)
|
|
if executable:
|
|
path.chmod(path.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
|
|
print(f" [OK] {path}")
|
|
return True
|
|
|
|
|
|
def _copy_dir(src: Path, dst: Path, cfg: OnboardingConfig, label: str = "") -> bool:
|
|
"""Copy a directory tree unless dry_run."""
|
|
if cfg.dry_run:
|
|
print(f" [DRY] {dst}/ ({label})")
|
|
return False
|
|
if not src.exists():
|
|
print(f" [SKIP] {dst}/ (source '{src}' does not exist)")
|
|
return False
|
|
shutil.copytree(src, dst, dirs_exist_ok=True)
|
|
print(f" [OK] {dst}/ ({label})")
|
|
return True
|
|
|
|
|
|
def _copy_file(src: Path, dst: Path, cfg: OnboardingConfig, executable: bool = False) -> bool:
|
|
if cfg.dry_run:
|
|
print(f" [DRY] {dst}")
|
|
return False
|
|
if not src.exists():
|
|
print(f" [SKIP] {dst} (source '{src}' does not exist)")
|
|
return False
|
|
shutil.copy2(src, dst)
|
|
if executable:
|
|
dst.chmod(dst.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
|
|
print(f" [OK] {dst}")
|
|
return True
|
|
|
|
|
|
def scaffold_client(cfg: OnboardingConfig) -> Path:
|
|
"""
|
|
Scaffold a complete client replica-omnisciente.
|
|
|
|
Args:
|
|
cfg: Fully populated OnboardingConfig.
|
|
|
|
Returns:
|
|
Path to the created project root.
|
|
|
|
Raises:
|
|
ScaffoldError: If validation fails or output dir already exists (non-dry-run).
|
|
"""
|
|
errors = cfg.validate()
|
|
if errors:
|
|
raise ScaffoldError("Validation failed:\n" + "\n".join(f" - {e}" for e in errors))
|
|
|
|
root = Path(cfg.output_dir)
|
|
src = Path(cfg.source_replica)
|
|
|
|
mode = "[DRY RUN] " if cfg.dry_run else ""
|
|
print(f"\n{'='*60}")
|
|
print(f"{mode}Scaffolding: {cfg.replica_name}")
|
|
print(f"{'='*60}")
|
|
print(f" Client: {cfg.client_name} ({cfg.client_slug})")
|
|
print(f" Output: {root}")
|
|
print(f" Source: {src}")
|
|
print(f" Git: {cfg.git_url or '(not set)'}")
|
|
print(f" Endpoint: {cfg.sync_endpoint or '(local-only)'}")
|
|
print(f" Realms: {len(cfg.realms)}")
|
|
print(f" Team: {1 + len(cfg.team_members)} heteronym(s)")
|
|
print()
|
|
|
|
if not cfg.dry_run:
|
|
if root.exists() and any(root.iterdir()):
|
|
raise ScaffoldError(f"Output directory already exists and is not empty: {root}")
|
|
root.mkdir(parents=True, exist_ok=True)
|
|
|
|
# ── Root files ──────────────────────────────────────────────
|
|
print("─ Root files ─")
|
|
_write(root / "AGENTS.md", templates.agents_md(cfg), cfg)
|
|
_write(root / "README.md", templates.readme_md(cfg), cfg)
|
|
_write(root / ".gitignore", templates.gitignore(), cfg)
|
|
_write(root / ".env.example", templates.env_example(cfg), cfg)
|
|
|
|
# ── .aurelio/ ───────────────────────────────────────────────
|
|
aurelio = root / ".aurelio"
|
|
print("\n─ .aurelio/ ─")
|
|
_write(aurelio / "config.toml", templates.aurelio_config_toml(cfg), cfg)
|
|
_write(aurelio / "mcp_config.json", templates.mcp_config_json(cfg), cfg)
|
|
|
|
# Brain (empty structure)
|
|
_write(aurelio / "brain" / ".gitkeep", "", cfg)
|
|
_write(aurelio / "memory" / "index.md",
|
|
f"# {cfg.replica_name} — Global Memory\n", cfg)
|
|
_write(aurelio / "plans" / ".gitkeep", "", cfg)
|
|
_write(aurelio / "sessions" / ".gitkeep", "", cfg)
|
|
|
|
# Sync script
|
|
if cfg.sync_endpoint or cfg.proxmox_host:
|
|
_write(aurelio / "sync.py", templates.sync_py(cfg), cfg, executable=True)
|
|
else:
|
|
_write(aurelio / "sync.py", templates.sync_py(cfg), cfg, executable=True)
|
|
|
|
# ── .aurelio/identity/ ──────────────────────────────────────
|
|
if cfg.include_identity:
|
|
print("\n─ .aurelio/identity/ ─")
|
|
ident = aurelio / "identity"
|
|
_write(ident / "heteronyms.json", templates.heteronyms_json(cfg), cfg)
|
|
_write(ident / "README.md", templates.identity_readme(cfg), cfg)
|
|
|
|
# Heteronym .md stubs
|
|
heteronimos = aurelio / "heteronimos"
|
|
if cfg.lead_engineer_name:
|
|
slug = cfg.lead_engineer_name.lower().replace(" ", "-")
|
|
_write(heteronimos / f"{slug}.md",
|
|
f"---\nslug: {slug}\nname: {cfg.lead_engineer_name}\nroleDefinition: Lead engineer for {cfg.client_name}.\n---\n## {cfg.lead_engineer_name}\n\n{cfg.lead_engineer_name} is the principal engineer for {cfg.client_name}.\n",
|
|
cfg)
|
|
for member in cfg.team_members:
|
|
slug = member["name"].lower().replace(" ", "-")
|
|
_write(heteronimos / f"{slug}.md",
|
|
f"---\nslug: {slug}\nname: {member['name']}\nroleDefinition: {member.get('role', 'Engineer')} for {cfg.client_name}.\n---\n## {member['name']}\n\n{member['name']} is a {member.get('role', 'engineer')} for {cfg.client_name}.\n",
|
|
cfg)
|
|
|
|
# ── .aurelio/providers/ ─────────────────────────────────────
|
|
if cfg.include_providers:
|
|
print("\n─ .aurelio/providers/ ─")
|
|
prov = aurelio / "providers"
|
|
_write(prov / "registry.yaml", templates.providers_registry_yaml(cfg), cfg)
|
|
_write(prov / "dist" / ".gitkeep", "", cfg)
|
|
|
|
# ── .aurelio/connectors/ ────────────────────────────────────
|
|
if cfg.include_connectors:
|
|
print("\n─ .aurelio/connectors/ ─")
|
|
conn = aurelio / "connectors"
|
|
_write(conn / "registry.yaml", templates.connectors_registry_yaml(cfg), cfg)
|
|
_write(conn / "dist" / ".gitkeep", "", cfg)
|
|
|
|
# ── .aurelio/skills/ ────────────────────────────────────────
|
|
if cfg.include_skills:
|
|
print("\n─ .aurelio/skills/ ─")
|
|
skills_dst = aurelio / "skills"
|
|
skills_src = src / ".aurelio" / "skills"
|
|
if skills_src.exists():
|
|
# Copy the full skills tree (these are portable CLI guides)
|
|
_copy_dir(skills_src, skills_dst, cfg, label=f"skills tree")
|
|
else:
|
|
_write(skills_dst / ".gitkeep", "", cfg)
|
|
|
|
# ── .aurelio/knowledge/ + chronicle/ ────────────────────────
|
|
_write(aurelio / "knowledge" / ".gitkeep", "", cfg)
|
|
chronicle = aurelio / "chronicle"
|
|
_write(chronicle / "README.md",
|
|
f"# Chronicle: {cfg.replica_name}\n\nTimeline of events across all realms.\n", cfg)
|
|
_write(chronicle / "timeline.yaml", "version: 1\nevents: []\n", cfg)
|
|
_write(chronicle / "realms" / ".gitkeep", "", cfg)
|
|
|
|
# ── .aurelio/swarm/ ─────────────────────────────────────────
|
|
_write(aurelio / "swarm" / "README.md",
|
|
f"# Swarm\n\nAgent fleet for {cfg.replica_name}.\n", cfg)
|
|
|
|
# ── realms/ ─────────────────────────────────────────────────
|
|
if cfg.realms:
|
|
print("\n─ realms/ ─")
|
|
for realm in cfg.realms:
|
|
rdir = root / "realms" / realm["slug"]
|
|
_write(rdir / "AGENTS.md", templates.realm_agents_md(cfg, realm), cfg)
|
|
ra = rdir / ".aurelio"
|
|
_write(ra / "config.toml", templates.realm_config_toml(cfg, realm), cfg)
|
|
_write(ra / "memory" / "index.md", templates.realm_memory_index(cfg, realm), cfg)
|
|
_write(ra / "plans" / ".gitkeep", "", cfg)
|
|
|
|
# ── scripts/ ────────────────────────────────────────────────
|
|
if cfg.include_scripts:
|
|
print("\n─ scripts/ ─")
|
|
scripts_dst = root / "scripts"
|
|
scripts_src = src / "scripts"
|
|
|
|
# Copy the agent-importer engine (portable, no project-specific deps)
|
|
_write(scripts_dst / "onboard-client.py", _onboard_cli_stub(cfg), cfg, executable=True)
|
|
|
|
# Copy sync-agents-to-brain.py if it exists
|
|
sync_agents = scripts_src / "sync-agents-to-brain.py"
|
|
if sync_agents.exists():
|
|
_copy_file(sync_agents, scripts_dst / "sync-agents-to-brain.py", cfg, executable=True)
|
|
|
|
# Copy agent_importers package
|
|
importers_src = scripts_src / "agent_importers"
|
|
if importers_src.exists():
|
|
_copy_dir(importers_src, scripts_dst / "agent_importers", cfg, label="agent importers")
|
|
|
|
# Copy onboarding package itself (self-replicating)
|
|
onboarding_src = scripts_src / "onboarding"
|
|
if onboarding_src.exists():
|
|
_copy_dir(onboarding_src, scripts_dst / "onboarding", cfg, label="onboarding package")
|
|
|
|
# Copy data_sharing package (consent-gated data transmission)
|
|
ds_src = scripts_src / "data_sharing"
|
|
if ds_src.exists():
|
|
_copy_dir(ds_src, scripts_dst / "data_sharing", cfg, label="data sharing layer")
|
|
ds_cli = scripts_src / "data-sharing.py"
|
|
if ds_cli.exists():
|
|
_copy_file(ds_cli, scripts_dst / "data-sharing.py", cfg, executable=True)
|
|
|
|
# Copy provider/connector mirror generators if they exist
|
|
for gen in ["generate-provider-mirrors.py", "generate-connector-mirrors.py"]:
|
|
gen_src = scripts_src / gen
|
|
if gen_src.exists():
|
|
_copy_file(gen_src, scripts_dst / gen, cfg, executable=True)
|
|
|
|
# ── CI/CD ───────────────────────────────────────────────────
|
|
if cfg.include_ci:
|
|
print("\n─ CI/CD ─")
|
|
if cfg.git_provider in ("forgejo", "codeberg"):
|
|
_write(root / ".forgejo" / "workflows" / "aurelio-sync.yml",
|
|
templates.forgejo_workflow(cfg), cfg)
|
|
if cfg.git_provider in ("github", "gitlab"):
|
|
_write(root / ".github" / "workflows" / "build.yml",
|
|
templates.github_workflow(cfg), cfg)
|
|
# Always include both so the client can switch forges later
|
|
if cfg.git_provider not in ("forgejo", "codeberg"):
|
|
_write(root / ".forgejo" / "workflows" / "aurelio-sync.yml",
|
|
templates.forgejo_workflow(cfg), cfg)
|
|
if cfg.git_provider not in ("github", "gitlab"):
|
|
_write(root / ".github" / "workflows" / "build.yml",
|
|
templates.github_workflow(cfg), cfg)
|
|
|
|
# ── Git init ────────────────────────────────────────────────
|
|
if not cfg.dry_run:
|
|
print("\n─ Git init ─")
|
|
_git_init(root, cfg)
|
|
|
|
print(f"\n{'='*60}")
|
|
mode = "[DRY RUN] " if cfg.dry_run else ""
|
|
print(f"{mode}Done: {cfg.replica_name}")
|
|
print(f"{'='*60}")
|
|
if not cfg.dry_run:
|
|
print(f"\nNext steps:")
|
|
print(f" cd {root}")
|
|
print(f" cp .env.example .env # fill in API keys")
|
|
if cfg.git_url:
|
|
print(f" git remote add {cfg.forge_remote_name} {cfg.git_url}")
|
|
print(f" git push -u {cfg.forge_remote_name} main")
|
|
print(f" # Read AGENTS.md for the full guide")
|
|
print()
|
|
|
|
return root
|
|
|
|
|
|
def _git_init(root: Path, cfg: OnboardingConfig):
|
|
"""Initialize git and make initial commit."""
|
|
import subprocess
|
|
|
|
try:
|
|
# Initialize repo
|
|
subprocess.run(["git", "init"], cwd=root, check=True, capture_output=True)
|
|
|
|
# Set per-repo identity if none is configured globally
|
|
has_identity = subprocess.run(
|
|
["git", "config", "user.email"],
|
|
cwd=root, capture_output=True, text=True
|
|
).returncode == 0
|
|
if not has_identity:
|
|
subprocess.run(["git", "config", "user.email",
|
|
cfg.lead_engineer_email or f"noreply@{cfg.client_slug}.com"],
|
|
cwd=root, check=True, capture_output=True)
|
|
subprocess.run(["git", "config", "user.name",
|
|
cfg.lead_engineer_name or cfg.client_name],
|
|
cwd=root, check=True, capture_output=True)
|
|
|
|
subprocess.run(["git", "add", "-A"], cwd=root, check=True, capture_output=True)
|
|
subprocess.run(
|
|
["git", "commit", "-m",
|
|
f"feat: initial scaffold of {cfg.replica_name}\n\n"
|
|
f"Scaffolded from Réplica Omnisciente template.\n"
|
|
f"Client: {cfg.client_name}"],
|
|
cwd=root, check=True, capture_output=True
|
|
)
|
|
if cfg.git_url:
|
|
subprocess.run(
|
|
["git", "remote", "add", cfg.forge_remote_name, cfg.git_url],
|
|
cwd=root, check=True, capture_output=True
|
|
)
|
|
print(f" [OK] git initialized + initial commit")
|
|
except FileNotFoundError:
|
|
print(f" [WARN] git not found — skipping git init")
|
|
except subprocess.CalledProcessError as e:
|
|
print(f" [WARN] git init failed: {e.stderr.decode() if e.stderr else e}")
|
|
|
|
|
|
def _onboard_cli_stub(cfg: OnboardingConfig) -> str:
|
|
"""Generate a self-contained CLI script for the scaffolded client."""
|
|
return f'''#!/usr/bin/env python3
|
|
"""
|
|
{cfg.replica_name} — Onboarding & Realm Management CLI
|
|
|
|
Usage:
|
|
python3 scripts/onboard-client.py --add-realm <slug> --name "Name" --repo <url>
|
|
python3 scripts/onboard-client.py --list-realms
|
|
python3 scripts/onboard-client.py --info
|
|
"""
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Ensure we can import the onboarding package
|
|
SCRIPTS_DIR = Path(__file__).parent
|
|
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
|
|
from onboarding import OnboardingConfig, scaffold_client
|
|
from onboarding.templates import realm_agents_md, realm_config_toml, realm_memory_index
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def add_realm(args):
|
|
"""Add a new realm to this replica."""
|
|
realm = {{
|
|
"slug": args.slug,
|
|
"name": args.name or args.slug,
|
|
"repo": args.repo or "",
|
|
"stack": args.stack or "",
|
|
"description": args.description or "",
|
|
}}
|
|
rdir = ROOT / "realms" / realm["slug"]
|
|
if rdir.exists() and not args.force:
|
|
print(f"Realm '{{realm['slug']}}' already exists. Use --force to overwrite.")
|
|
sys.exit(1)
|
|
|
|
# Load current config for template rendering
|
|
cfg = OnboardingConfig(client_name="{cfg.client_name}")
|
|
cfg.source_replica = str(ROOT)
|
|
|
|
rdir.mkdir(parents=True, exist_ok=True)
|
|
(rdir / "AGENTS.md").write_text(realm_agents_md(cfg, realm))
|
|
ra = rdir / ".aurelio"
|
|
ra.mkdir(parents=True, exist_ok=True)
|
|
(ra / "config.toml").write_text(realm_config_toml(cfg, realm))
|
|
(ra / "memory").mkdir(exist_ok=True)
|
|
(ra / "memory" / "index.md").write_text(realm_memory_index(cfg, realm))
|
|
(ra / "plans").mkdir(exist_ok=True)
|
|
|
|
print(f"Realm '{{realm['slug']}}' created at {{rdir}}")
|
|
print(f" AGENTS.md, .aurelio/config.toml, .aurelio/memory/index.md")
|
|
|
|
|
|
def list_realms(args):
|
|
"""List all realms."""
|
|
realms_dir = ROOT / "realms"
|
|
if not realms_dir.exists():
|
|
print("No realms directory.")
|
|
return
|
|
print("Realms:")
|
|
for d in sorted(realms_dir.iterdir()):
|
|
if d.is_dir():
|
|
agents_file = d / "AGENTS.md"
|
|
name = d.name
|
|
if agents_file.exists():
|
|
first_line = agents_file.read_text().splitlines()[0] if agents_file.read_text() else ""
|
|
name = first_line.replace("#", "").strip() or d.name
|
|
print(f" - {{d.name}}: {{name}}")
|
|
|
|
|
|
def info(args):
|
|
"""Show replica info."""
|
|
cfg = OnboardingConfig(client_name="{cfg.client_name}")
|
|
print(f"Replica: {{cfg.replica_name}}")
|
|
print(f"Client: {{cfg.client_name}} ({{cfg.client_slug}})")
|
|
print(f"Root: {{ROOT}}")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="{cfg.replica_name} — Onboarding CLI")
|
|
sub = parser.add_subparsers(dest="command")
|
|
|
|
p_add = sub.add_parser("add-realm", help="Add a new realm")
|
|
p_add.add_argument("slug", help="Realm slug (e.g. 'my-project')")
|
|
p_add.add_argument("--name", help="Human-readable name")
|
|
p_add.add_argument("--repo", help="Git repository URL")
|
|
p_add.add_argument("--stack", help="Technology stack summary")
|
|
p_add.add_argument("--description", help="One-line description")
|
|
p_add.add_argument("--force", action="store_true", help="Overwrite if exists")
|
|
p_add.set_defaults(func=add_realm)
|
|
|
|
p_list = sub.add_parser("list-realms", help="List all realms")
|
|
p_list.set_defaults(func=list_realms)
|
|
|
|
p_info = sub.add_parser("info", help="Show replica info")
|
|
p_info.set_defaults(func=info)
|
|
|
|
args = parser.parse_args()
|
|
if not args.command:
|
|
parser.print_help()
|
|
sys.exit(1)
|
|
args.func(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
'''
|