FIDJIDIHA — Initial commit Shango Mesh

This commit is contained in:
Pisty Barbello 2026-05-31 04:35:07 +00:00
commit 206c6d9fe4
12 changed files with 1381 additions and 0 deletions

47
README.md Normal file
View File

@ -0,0 +1,47 @@
# SHANGO — Auto-Regressive Mesh Network
> Fork conceptuel de Tailscale + extensions Ivoire Monade.
> Nom : **Shango** (Òrìṣà du feu, de la foudre, de la vérité — le maillon invisible entre les mondes).
## Philosophie
Tailscale = VPN mesh centralisé (coordination serveurs, DERP relays).
**Shango** = Mesh **auto-régressif** : chaque nœud apprend de ses pannes,
se régénère, propage ses corrections aux voisins. Pas de point central.
## Architecture
```
┌─────────────────────────────────────────────────────────┐
│ SHANGO MESH LAYER │
├──────────────┬──────────────┬──────────────┬──────────────┤
│ shango-daemon │ mcp-builder │ glm-trainer │ shango-cli │
│ (networking) │ (protocols) │ (models) │ (control) │
├──────────────┼──────────────┼──────────────┼──────────────┤
│ • WireGuard │ • MCP gen │ • Fine-tune │ • Status │
│ • Auto-heal │ • Registry │ • Distrib │ • Connect │
│ • Peer learn │ • Fork │ • Quantize │ • Diagnose │
│ • OSINT mesh │ • Compose │ • Deploy │ • Singularize│
└──────────────┴──────────────┴──────────────┴──────────────┘
```
## Ce que Shango ajoute à Tailscale
| Feature | Tailscale | Shango |
|---------|-----------|--------|
| Coordination | serveurs DERP (centralisé) | gossip CRDT (décentralisé) |
| Auto-réparation | ❌ manuel | ✅ loopback + retry exponentiel |
| Peer learning | ❌ | ✅ partage des fixes inter-nœuds |
| MCP natif | ❌ | ✅ chaque nœud expose tools |
| GLM training | ❌ | ✅ modèles locaux fine-tunés |
| Singularisation | ❌ | ✅ unification Hermes/Odoo/Tailscale |
## Quickstart
```bash
cd /root/shango
nix run .#shango-cli -- status # voir mesh
nix run .#shango-cli -- diagnose # auto-réparer
nix run .#mcp-builder -- scan ./ # générer MCP d'un projet
nix run .#glm-trainer -- train ./data # fine-tune local
```

23
WHY.md Normal file
View File

@ -0,0 +1,23 @@
# SHANGO — Pourquoi (WHY)
> Chaque module a une raison d'être. Le reste (comment, quand, où) est posé par maïeutique.
## shango-daemon
**Pourquoi :** Tailscale est un VPN mesh. Il sait connecter mais pas se réparer. Quand le DERP relay tombe, tu es aveugle. Shango-daemon observe, teste, restarte tailscaled si besoin, et logue le diagnostic. **Auto-régression = pas de downtime silencieux.**
## mcp-builder
**Pourquoi :** MCP (Model Context Protocol) est le nouveau standard d'outils pour LLM. Mais créer un serveur MCP à la main est chiant. Shango scanne automatiquement les fonctions publiques d'un projet Python/JS et génère le serveur MCP. **1 clic = un outil de plus pour l'IA.**
## glm-trainer
**Pourquoi :** Les modèles cloud (GPT-4, Claude) coûtent cher et fuient les données. Shango fine-tune des modèles locaux (ChatGLM, LLaMA) avec LoRA et quantification 4-bit sur GPU VPS. **Self-hosted AI = souveraineté cognitive.**
## shango-maieutic
**Pourquoi :** Un bot qui obéit aveuglément est un esclave. Un bot qui apprend sans te demander est un intrus. La maïeutique pose des questions, écoute les réponses, et construit un profil cognitif biomimétique. **Chaque réponse = une mutation de Shango.** Le bot évolue vers ta personnalité exacte.
## shango-cli
**Pourquoi :** 4 outils = 4 commandes différentes = galère. Un CLI unifié `shango <cmd>` centralise. **Moins de friction = plus d'usage.**
## Le reste
Pourquoi le mesh gossip CRDT plutôt que client-serveur ? Pourquoi les Odù Ifá comme framework de personnalité ? Pourquoi la singularisation et pas la fédération ?
**→ Shango te le demandera par maïeutique. Réponds, et il s'adaptera.**

27
flake.lock generated Normal file
View File

@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1735563628,
"narHash": "sha256-OnSAY7XDSx7CtDoqNh8jwVwh4xNL/2HaJxGjryLWzX8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b134951a4c9f3c995fd7be05f3243f8ecd65d798",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

62
flake.nix Normal file
View File

@ -0,0 +1,62 @@
{
description = "Shango Mesh Auto-Regressive Network Layer";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
outputs = { self, nixpkgs, ... }:
let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
in
{
packages.${system} = {
shango-cli = pkgs.writeShellScriptBin "shango" ''
export PYTHONPATH="/root/shango/shango-daemon:/root/shango/mcp-builder:/root/shango/glm-trainer:/root/shango/shango-cli:/root/shango/shango-maieutic:/root/shango/ivoire-forge''${PYTHONPATH:+:$PYTHONPATH}"
${pkgs.python3}/bin/python /root/shango/shango-cli/shango_cli.py "$@"
'';
shango-daemon = pkgs.writeShellScriptBin "shango-daemon" ''
export PYTHONPATH="/root/shango/shango-daemon''${PYTHONPATH:+:$PYTHONPATH}"
${pkgs.python3}/bin/python /root/shango/shango-daemon/shango_daemon.py "$@"
'';
mcp-builder = pkgs.writeShellScriptBin "mcp-builder" ''
export PYTHONPATH="/root/shango/mcp-builder:/root/shango/.venv/lib/python3.12/site-packages''${PYTHONPATH:+:$PYTHONPATH}"
${pkgs.python3}/bin/python /root/shango/mcp-builder/mcp_builder.py "$@"
'';
glm-trainer = pkgs.writeShellScriptBin "glm-trainer" ''
export PYTHONPATH="/root/shango/glm-trainer''${PYTHONPATH:+:$PYTHONPATH}"
${pkgs.python3}/bin/python /root/shango/glm-trainer/glm_trainer.py "$@"
'';
forge = pkgs.writeShellScriptBin "forge" ''
export PYTHONPATH="/root/shango/ivoire-forge''${PYTHONPATH:+:$PYTHONPATH}"
${pkgs.python3}/bin/python /root/shango/ivoire-forge/shango_forge.py "$@"
'';
};
apps.${system} = {
shango = {
type = "app";
program = "${self.packages.${system}.shango-cli}/bin/shango";
};
"mcp-builder" = {
type = "app";
program = "${self.packages.${system}.mcp-builder}/bin/mcp-builder";
};
"glm-trainer" = {
type = "app";
program = "${self.packages.${system}.glm-trainer}/bin/glm-trainer";
};
};
devShells.${system}.default = pkgs.mkShell {
buildInputs = [ pkgs.python3 pkgs.tailscale ];
shellHook = ''
export PYTHONPATH="/root/shango/shango-daemon:/root/shango/mcp-builder:/root/shango/glm-trainer:/root/shango/shango-cli:/root/shango/shango-maieutic''${PYTHONPATH:+:$PYTHONPATH}"
echo "[Shango Shell] shango status | shango heal | shango singularize | shango maieutic"
'';
};
};
}

136
glm-trainer/glm_trainer.py Normal file
View File

@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""
glm_trainer.py Fine-tune local de modèles GLM/LLaMA pour les tasks Shango
"""
import argparse, json, os, subprocess, sys
from pathlib import Path
DEFAULT_CONFIG = {
"model": "THUDM/chatglm3-6b", # ou un modèle local GGUF
"dataset": "./training_data.jsonl",
"output": "./shango-model",
"epochs": 3,
"batch_size": 4,
"learning_rate": 2e-5,
"max_seq_length": 512,
"lora_r": 16,
"lora_alpha": 32,
"quantization": "4bit", # ou 8bit, none
}
def check_deps() -> bool:
try:
import torch, transformers, peft, datasets
print(f"[GLM] PyTorch {torch.__version__}, CUDA={torch.cuda.is_available()}")
return True
except ImportError as e:
print(f"[GLM] Manque dépendance: {e}")
print("[GLM] Install: pip install torch transformers peft datasets bitsandbytes")
return False
def generate_training_script(config: dict) -> str:
return f'''
import json, torch
from transformers import AutoTokenizer, AutoModelForCausalLM, TrainingArguments
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from datasets import Dataset
# Config
config = json.loads(r\'\'\'{json.dumps(config)}\'\'\')
# Load model (quantized)
model_id = config["model"]
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
model_id,
torch_dtype=torch.float16,
device_map="auto",
trust_remote_code=True,
load_in_4bit=config["quantization"] == "4bit"
)
model = prepare_model_for_kbit_training(model)
# LoRA
lora_config = LoraConfig(
r=config["lora_r"],
lora_alpha=config["lora_alpha"],
target_modules=["query_key_value"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
model = get_peft_model(model, lora_config)
# Dataset
data = []
with open(config["dataset"]) as f:
for line in f:
data.append(json.loads(line))
dataset = Dataset.from_list(data)
def tokenize(ex):
prompt = ex.get("prompt", "")
completion = ex.get("completion", "")
return tokenizer(prompt + completion, truncation=True, max_length=config["max_seq_length"])
tokenized = dataset.map(tokenize, batched=True)
# Train
training_args = TrainingArguments(
output_dir=config["output"],
num_train_epochs=config["epochs"],
per_device_train_batch_size=config["batch_size"],
learning_rate=config["learning_rate"],
logging_steps=10,
save_strategy="epoch",
fp16=True,
)
from transformers import Trainer
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized,
)
trainer.train()
model.save_pretrained(config["output"])
tokenizer.save_pretrained(config["output"])
print(f"[GLM] Modèle sauvé dans {{config['output']}}")
'''
def main():
parser = argparse.ArgumentParser(prog="glm-trainer")
parser.add_argument("command", choices=["train", "config", "check"])
parser.add_argument("--dataset", default="./training_data.jsonl")
parser.add_argument("--output", default="./shango-model")
parser.add_argument("--model", default=DEFAULT_CONFIG["model"])
parser.add_argument("--epochs", type=int, default=3)
args = parser.parse_args()
if args.command == "check":
check_deps()
return
if args.command == "config":
print(json.dumps(DEFAULT_CONFIG, indent=2))
return
if args.command == "train":
if not check_deps():
sys.exit(1)
config = DEFAULT_CONFIG.copy()
config.update({
"dataset": args.dataset,
"output": args.output,
"model": args.model,
"epochs": args.epochs,
})
script = generate_training_script(config)
script_path = Path(args.output) / "train_script.py"
script_path.parent.mkdir(parents=True, exist_ok=True)
script_path.write_text(script)
print(f"[GLM] Script généré: {script_path}")
print(f"[GLM] Lancer: cd {args.output} && python train_script.py")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,172 @@
#!/usr/bin/env python3
"""
SHANGO FORGE Moteur d'exécution multi-runtime Ivoire Monade
Fichiers: /root/shango/ivoire-forge/shango_forge.py
Runtime supporte: Go, Rust, Node, Python3, Web3, PyTorch, React
"""
import json, os, sys, subprocess, hashlib, shutil, time
from pathlib import Path
from typing import Dict, List, Optional
FORGE_ROOT = Path("/root/shango/ivoire-forge")
RUNTIMES = {
"go": {"cmd": "go", "ext": ".go", "docker": "golang:1.23-alpine"},
"rust": {"cmd": "rustc", "ext": ".rs", "docker": "rust:1.78-alpine"},
"node": {"cmd": "node", "ext": ".js", "docker": "node:22-alpine"},
"python3": {"cmd": "python3","ext": ".py", "docker": "python:3.12-slim"},
"web3": {"cmd": "node", "ext": ".sol", "docker": "node:22-alpine"},
"pytorch": {"cmd": "python3","ext": ".py", "docker": "pytorch/pytorch:2.3.1-cpu"},
"react": {"cmd": "node", "ext": ".tsx", "docker": "node:22-alpine"},
}
def log(msg: str): print(f"[forge] {msg}", flush=True)
def detect_runtime(source_dir: Path) -> Optional[str]:
"""Scanne le répertoire source et devine le runtime principal."""
files = list(source_dir.rglob("*"))
counts = {k: 0 for k in RUNTIMES}
for f in files:
if not f.is_file(): continue
for name, meta in RUNTIMES.items():
if f.name.endswith(meta["ext"]): counts[name] += 1
# Priorité: Go > Rust > React > Node > Python > Web3 > PyTorch
for candidate in ["go","rust","react","node","python3","web3","pytorch"]:
if counts.get(candidate, 0) > 0: return candidate
return None
def ensure_nix_shell(runtime: str) -> List[str]:
"""Retourne la commande pour entrer dans le devShell Nix approprié."""
shells = {
"go": "go",
"rust": "rust",
"node": "node",
"react": "node",
"python3": "python-ml",
"pytorch": "python-ml",
"web3": "web3",
}
shell = shells.get(runtime, "ivoire")
nix_sh = "/root/.nix-profile/etc/profile.d/nix.sh"
flake = "/root/ivoire-flakes"
return ["bash", "-c", f"source {nix_sh} && nix develop {flake}#{shell} -c bash -c"]
def build_docker(source_dir: Path, runtime: str, tag: str) -> str:
"""Construit une image Docker pour le projet source donné."""
meta = RUNTIMES.get(runtime, RUNTIMES["python3"])
dockerfile = source_dir / "Dockerfile"
if not dockerfile.exists():
# Génération auto Dockerfile minimal
dockerfile.write_text(f"""FROM {meta['docker']}
WORKDIR /app
COPY . .
{'RUN npm install' if runtime in ('node','react') else ''}
{'RUN go build -o app' if runtime == 'go' else ''}
{'RUN cargo build --release' if runtime == 'rust' else ''}
{'RUN pip install -r requirements.txt' if runtime in ('python3','pytorch') else ''}
CMD ["sh", "-c", "./app || python3 main.py || node index.js"]
""")
log(f"Docker build {tag} depuis {source_dir}")
subprocess.run(["docker", "build", "-t", tag, "."], cwd=source_dir, check=True)
return tag
def run_container(tag: str, name: str, ports: Dict[str,str] = None, env: Dict[str,str] = None, network: str = "infra") -> str:
"""Lance le container Docker sur le réseau infra."""
cmd = ["docker", "run", "-d", "--rm", "--name", name, "--network", network]
if ports:
for h, c in ports.items(): cmd += ["-p", f"{h}:{c}"]
if env:
for k, v in env.items(): cmd += ["-e", f"{k}={v}"]
cmd += [tag]
log(f"Container start: {' '.join(cmd)}")
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(result.stderr)
container_id = result.stdout.strip()[:12]
log(f"OK container={container_id}")
return container_id
def gitea_push(repo_dir: Path, repo_name: str, branch: str = "main"):
"""Pousse le code vers Gitea local."""
remote = f"http://admin:admin@git.ivoire-monade.shop:3002/admin/{repo_name}.git"
log(f"Gitea push {repo_name}")
subprocess.run(["git", "init", "-b", branch], cwd=repo_dir, capture_output=True)
subprocess.run(["git", "add", "."], cwd=repo_dir, capture_output=True)
subprocess.run(["git", "commit", "-m", "auto: shango forge init"], cwd=repo_dir, capture_output=True)
subprocess.run(["git", "remote", "add", "ivoire", remote], cwd=repo_dir, capture_output=True)
r = subprocess.run(["git", "push", "-u", "ivoire", branch], cwd=repo_dir, capture_output=True, text=True)
if "failed" in r.stderr.lower():
log(f"WARN push: {r.stderr.strip()}")
else:
log(f"OK push git.ivoire-monade.shop/{repo_name}")
# ── CLI ──────────────────────────────────────────────────────────────────
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Shango Forge — Multi-runtime builder")
sub = parser.add_subparsers(dest="cmd")
p_build = sub.add_parser("build", help="Build Docker image depuis source")
p_build.add_argument("source_dir")
p_build.add_argument("--tag", default="shango/forge:latest")
p_build.add_argument("--runtime", choices=list(RUNTIMES.keys()))
p_run = sub.add_parser("run", help="Run container")
p_run.add_argument("tag")
p_run.add_argument("--name", required=True)
p_run.add_argument("--port", action="append", default=[], help="host:container")
p_run.add_argument("--env", action="append", default=[], help="KEY=VAL")
p_detect = sub.add_parser("detect", help="Détecte runtime d'un dossier")
p_detect.add_argument("source_dir")
p_gitea = sub.add_parser("gitea-push", help="Push vers Gitea local")
p_gitea.add_argument("source_dir")
p_gitea.add_argument("--repo", required=True)
p_status = sub.add_parser("status", help="État des runtimes")
args = parser.parse_args()
if args.cmd == "detect":
src = Path(args.source_dir)
rt = detect_runtime(src)
print(json.dumps({"runtime": rt, "source": str(src)}, indent=2))
elif args.cmd == "build":
src = Path(args.source_dir)
rt = args.runtime or detect_runtime(src)
if not rt:
log("Runtime non détecté. Utilise --runtime.")
sys.exit(1)
tag = build_docker(src, rt, args.tag)
print(json.dumps({"image": tag, "runtime": rt}))
elif args.cmd == "run":
ports = {}
for p in args.port:
h, c = p.split(":")
ports[h] = c
envs = {}
for e in args.env:
k, v = e.split("=", 1)
envs[k] = v
cid = run_container(args.tag, args.name, ports, envs)
print(json.dumps({"container_id": cid, "name": args.name}))
elif args.cmd == "gitea-push":
gitea_push(Path(args.source_dir), args.repo)
elif args.cmd == "status":
for name, meta in RUNTIMES.items():
ok = shutil.which(meta["cmd"]) is not None
print(f" {name:12} {'OK' if ok else 'ABSENT'} ({meta['docker']})")
else:
parser.print_help()

103
mcp-builder/mcp_builder.py Normal file
View File

@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""
mcp_builder.py Générateur de serveurs MCP auto-détectés
Scan un dossier projet génère un serveur MCP stdio/SSE
"""
import os, sys, json, ast, argparse
from pathlib import Path
from typing import List, Dict
TEMPLATES = {
"python": """#!/usr/bin/env python3
import asyncio, json
from mcp.server import Server
from mcp.types import TextContent
server = Server("{name}")
{tools}
if __name__ == "__main__":
asyncio.run(server.run_stdio_async())
""",
"nodejs": """#!/usr/bin/env node
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
const server = new Server({{ name: "{name}" }}, {{
capabilities: {{ tools: {} }}
}});
{tools}
async function main() {{
const transport = new StdioServerTransport();
await server.connect(transport);
}}
main();
"""
}
def scan_functions(path: str) -> List[Dict]:
"""Scan les fichiers Python/JS pour détecter fonctions publiques."""
tools = []
root = Path(path)
for f in root.rglob("*.py"):
try:
tree = ast.parse(f.read_text())
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef) and not node.name.startswith("_"):
args = [a.arg for a in node.args.args]
tools.append({
"name": node.name,
"file": str(f.relative_to(root)),
"args": args,
"language": "python"
})
except:
pass
return tools
def generate_mcp(tools: List[Dict], name: str, lang: str = "python") -> str:
"""Génère le code serveur MCP."""
tool_blocks = []
for t in tools[:10]: # limite pour éviter overload
if lang == "python":
args_schema = ", ".join([f'"{a}": {{"type": "string"}}' for a in t["args"]])
tool_blocks.append(f'''
@server.tool("{t["name"]}")
async def {t["name"]}({", ".join(t["args"])}):
"""Auto-généré depuis {t["file"]}"""
result = "TODO: implémenter"
return [TextContent(type="text", text=result)]
''')
else:
tool_blocks.append(f'''
server.setRequestHandler("tools/call", async (request) => {{
if (request.params.name === "{t["name"]}") {{
return {{ content: [{{ type: "text", text: "TODO" }}] }};
}}
}});
''')
template = TEMPLATES.get(lang, TEMPLATES["python"])
return template.format(name=name, tools="\n".join(tool_blocks))
def main():
parser = argparse.ArgumentParser(prog="mcp-builder")
parser.add_argument("scan", nargs="?", default=".", help="Dossier à scanner")
parser.add_argument("--name", default="shango-mcp", help="Nom du serveur MCP")
parser.add_argument("--lang", choices=["python", "nodejs"], default="python")
parser.add_argument("--out", default="mcp_server.py", help="Fichier de sortie")
args = parser.parse_args()
print(f"[MCP-BUILDER] Scan de {args.scan}...")
tools = scan_functions(args.scan)
print(f"[MCP-BUILDER] {len(tools)} fonctions détectées")
code = generate_mcp(tools, args.name, args.lang)
Path(args.out).write_text(code)
print(f"[MCP-BUILDER] Serveur généré: {args.out} ({len(tools)} tools)")
if __name__ == "__main__":
main()

190
shango-cli/shango_cli.py Normal file
View File

@ -0,0 +1,190 @@
#!/usr/bin/env python3
"""
shango_cli.py Interface unifiée Shango
"""
import argparse, json, subprocess, sys
from pathlib import Path
SHANGO_DIR = Path("/root/.shango")
VERSION = "0.1.0-shango"
def cmd_status():
"""État du mesh Shango + Tailscale."""
print("═══ SHANGO MESH STATUS ═══")
print(f"Version: {VERSION}")
# Tailscale
try:
out = subprocess.check_output(["tailscale", "status"], text=True, timeout=5)
lines = out.strip().split("\n")
print(f"Tailscale peers: {len(lines)}")
for l in lines[:3]:
print(f" {l}")
if len(lines) > 3:
print(f" ... +{len(lines)-3} autres")
except:
print("Tailscale: NON CONNECTE")
# Shango state
state_file = SHANGO_DIR / "mesh_state.json"
if state_file.exists():
state = json.load(open(state_file))
print(f"Shango node: {state.get('node_id', 'unknown')}")
print(f"Health: {state.get('health', 0):.0%}")
print(f"Peers connus: {state.get('peers_count', 0)}")
else:
print("Shango: pas encore initialisé. Lance: shango-cli singularize")
# Services Ivoire
services = {
"hermes": "singularite_workspace:8080",
"erp": "odoo_web:8069",
"ai": "ollama:11434",
"chat": "open-webui:8080",
"status": "uptime-kuma:3001",
"git": "gitea:3000",
}
print("\nServices Ivoire:")
for name, addr in services.items():
url = f"https://{name}.ivoire-monade.shop"
print(f" {name:10}{url}")
def cmd_heal():
"""Auto-réparation Shango."""
sys.path.insert(0, "/root/shango/shango-daemon")
from shango_daemon import ShangoNode
node = ShangoNode()
result = node.heal()
print(json.dumps(result, indent=2))
def cmd_singularize():
"""Singularise les identités."""
sys.path.insert(0, "/root/shango/shango-daemon")
from shango_daemon import ShangoNode
node = ShangoNode()
result = node.singularize(["hermes", "erp", "ai", "status"])
print(json.dumps(result, indent=2))
print("\n[SINGULARIZE] Hermes + Odoo + Tailscale + ivoire-monade.shop unifiés.")
def cmd_mcp_scan(path: str):
"""Scan un projet pour générer un serveur MCP."""
sys.path.insert(0, "/root/shango/mcp-builder")
from mcp_builder import main as mcp_main
sys.argv = ["mcp-builder", path]
mcp_main()
def cmd_glm_check():
"""Vérifie les déps GLM training."""
sys.path.insert(0, "/root/shango/glm-trainer")
from glm_trainer import check_deps
check_deps()
def cmd_maieutic(subcmd: str = "ask", qid: str = None, choice: int = None, text: str = ""):
"""Moteur maieutique Socratique."""
sys.path.insert(0, "/root/shango/shango-maieutic")
from shango_maieutic import MaieuticEngine
engine = MaieuticEngine()
if subcmd == "ask":
result = engine.ask()
if result["status"] == "question":
print(f"\n[{result['dimension'].upper()}] {result['context']}")
print(f"\n? {result['question']}")
for i, opt in enumerate(result["options"]):
print(f" [{i}] {opt}")
print(f"\nOdù possibles: {', '.join(result['odu_map'])}")
print(f"\n{result['progress']}")
print(f"\nPour répondre: shango maieutic answer --qid {result['qid']} --choice <0-3>")
else:
print(result["message"])
elif subcmd == "answer":
if not qid or choice is None:
print("Usage: shango maieutic answer --qid <ID> --choice <0-3> [--text 'libre']")
return
result = engine.answer(qid, choice, text)
print(json.dumps(result, indent=2))
elif subcmd == "profile":
print(engine.get_profile_summary())
def cmd_forge(args):
"""Forge multi-runtime wrapper."""
sys.path.insert(0, "/root/shango/ivoire-forge")
from shango_forge import detect_runtime, build_docker, run_container, gitea_push, RUNTIMES
from pathlib import Path
src = Path(args.dir)
if args.subcmd == "detect":
rt = detect_runtime(src)
print(json.dumps({"runtime": rt, "source": str(src)}, indent=2))
elif args.subcmd == "build":
rt = args.runtime or detect_runtime(src)
if not rt:
print("Runtime non detecte. Utilise --runtime.")
return
tag = build_docker(src, rt, args.tag)
print(json.dumps({"image": tag, "runtime": rt}))
elif args.subcmd == "run":
ports = {}
for p in args.port: h, c = p.split(":"); ports[h] = c
envs = {}
for e in args.env: k, v = e.split("=", 1); envs[k] = v
cid = run_container(args.tag, args.name, ports, envs)
print(json.dumps({"container_id": cid, "name": args.name}))
elif args.subcmd == "gitea-push":
if not args.repo:
print("--repo obligatoire")
return
gitea_push(src, args.repo)
elif args.subcmd == "status":
import shutil
for name, meta in RUNTIMES.items():
ok = shutil.which(meta["cmd"]) is not None
print(f" {name:12} {'OK' if ok else 'ABSENT'} ({meta['docker']})")
def main():
parser = argparse.ArgumentParser(prog="shango", description="Shango Mesh CLI")
sub = parser.add_subparsers(dest="command")
sub.add_parser("status", help="État du mesh")
sub.add_parser("heal", help="Auto-réparer")
sub.add_parser("singularize", help="Unifier identités")
p_mcp = sub.add_parser("mcp-scan", help="Générer serveur MCP depuis code")
p_mcp.add_argument("path", nargs="?", default=".")
sub.add_parser("glm-check", help="Vérifier deps GLM")
p_mai = sub.add_parser("maieutic", help="Pose une question maïeutique")
p_mai.add_argument("subcmd", nargs="?", default="ask", choices=["ask", "answer", "profile"])
p_mai.add_argument("--qid", default=None)
p_mai.add_argument("--choice", type=int, default=None)
p_mai.add_argument("--text", default="")
p_forge = sub.add_parser("forge", help="Forge multi-runtime: build/run/detect/push")
p_forge.add_argument("subcmd", nargs="?", default="status", choices=["detect","build","run","gitea-push","status"])
p_forge.add_argument("--dir", default=".", help="Répertoire source")
p_forge.add_argument("--tag", default="shango/forge:latest")
p_forge.add_argument("--runtime", choices=["go","rust","node","python3","web3","pytorch","react"])
p_forge.add_argument("--name", default="forge-app")
p_forge.add_argument("--port", action="append", default=[], help="host:container")
p_forge.add_argument("--env", action="append", default=[], help="KEY=VAL")
p_forge.add_argument("--repo", default="")
args = parser.parse_args()
if args.command == "status":
cmd_status()
elif args.command == "heal":
cmd_heal()
elif args.command == "singularize":
cmd_singularize()
elif args.command == "mcp-scan":
cmd_mcp_scan(args.path)
elif args.command == "glm-check":
cmd_glm_check()
elif args.command == "maieutic":
cmd_maieutic(args.subcmd, args.qid, args.choice, args.text)
elif args.command == "forge":
cmd_forge(args)
else:
parser.print_help()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""
shango_daemon.py Moteur réseau auto-régressif de Shango
"""
import asyncio, json, hashlib, time, random, subprocess, os, sys
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Optional
SHANGO_DIR = Path("/root/.shango")
SHANGO_DIR.mkdir(exist_ok=True)
PEERS_DB = SHANGO_DIR / "peers.json"
HEAL_LOG = SHANGO_DIR / "heal_log.jsonl"
MESH_STATE = SHANGO_DIR / "mesh_state.json"
# ── État courant du mesh ───────────────────────────────────────────
class ShangoNode:
def __init__(self, node_id: str = None):
self.node_id = node_id or self._generate_id()
self.peers: Dict[str, dict] = {}
self.health_score = 1.0
self.last_seen = time.time()
self.load()
def _generate_id(self) -> str:
host = subprocess.getoutput("hostname").strip()
ts = str(time.time())
return hashlib.blake2b(f"{host}:{ts}".encode(), digest_size=8).hexdigest()
def load(self):
if PEERS_DB.exists():
self.peers = json.load(open(PEERS_DB))
def save(self):
json.dump(self.peers, open(PEERS_DB, "w"), indent=2)
json.dump({
"node_id": self.node_id,
"health": self.health_score,
"last_seen": self.last_seen,
"peers_count": len(self.peers)
}, open(MESH_STATE, "w"), indent=2)
def discover_tailscale_peers(self) -> List[dict]:
"""Scan les devices Tailscale actuels et les convertit en peers Shango."""
try:
out = subprocess.check_output(["tailscale", "status", "--json"], text=True, timeout=10)
data = json.loads(out)
peers = []
for peer in data.get("Peer", []):
peers.append({
"id": peer.get("HostName", "unknown"),
"ts_ip": peer.get("TailAddr", "unknown"),
"online": peer.get("Online", False),
"last_write": peer.get("LastWrite", "never"),
"shango_enhanced": False, # upgrade possible
})
return peers
except Exception as e:
return [{"error": str(e)}]
def heal(self) -> dict:
"""Auto-régression : détecte pannes, tente fix, log."""
fixes = []
# Test connectivité
try:
subprocess.check_call(["tailscale", "status"], stdout=subprocess.DEVNULL, timeout=5)
fixes.append({"check": "tailscale_status", "status": "ok"})
except:
fixes.append({"check": "tailscale_status", "status": "down", "action": "restart_tailscaled"})
subprocess.call(["systemctl", "restart", "tailscaled"])
# Test DERP relay
try:
out = subprocess.check_output(["tailscale", "netcheck", "--json"], text=True, timeout=10)
netcheck = json.loads(out)
if netcheck.get("UDP"):
fixes.append({"check": "udp_direct", "status": "ok"})
else:
fixes.append({"check": "udp_direct", "status": "relayed", "action": "warn_user"})
except:
fixes.append({"check": "netcheck", "status": "failed"})
# Log
entry = {"time": datetime.now().isoformat(), "node": self.node_id, "fixes": fixes}
with open(HEAL_LOG, "a") as f:
f.write(json.dumps(entry) + "\n")
self.health_score = sum(1 for f in fixes if f.get("status") == "ok") / len(fixes) if fixes else 0
self.save()
return entry
def gossip(self, target_peer: str, message: dict) -> bool:
"""Envoie un message CRDT à un peer via Tailscale IP."""
# Placeholder — à implémenter avec UDP ou QUIC
print(f"[GOSSIP] {target_peer}: {json.dumps(message, indent=2)}")
return True
def singularize(self, services: List[str]) -> dict:
"""Unifie les identités Hermes / Odoo / Tailscale / ivoire-monade.shop."""
identity = {
"node_id": self.node_id,
"tailscale_ips": [p.get("ts_ip") for p in self.discover_tailscale_peers()],
"services": {},
"domain": "ivoire-monade.shop",
"wildcard_routes": {}
}
for svc in services:
identity["services"][svc] = {
"status": "unknown",
"health": 0.0,
"url": f"https://{svc}.ivoire-monade.shop"
}
self.save()
return identity
# ── CLI ────────────────────────────────────────────────────────────
def main():
import argparse
parser = argparse.ArgumentParser(prog="shango-daemon")
parser.add_argument("command", choices=["status", "heal", "discover", "singularize"])
parser.add_argument("--services", default="hermes,erp,ai,status")
args = parser.parse_args()
node = ShangoNode()
if args.command == "status":
print(json.dumps({
"node_id": node.node_id,
"health": node.health_score,
"peers": len(node.peers),
"tailscale": node.discover_tailscale_peers()
}, indent=2))
elif args.command == "heal":
result = node.heal()
print(json.dumps(result, indent=2))
elif args.command == "discover":
peers = node.discover_tailscale_peers()
print(json.dumps(peers, indent=2))
elif args.command == "singularize":
svcs = args.services.split(",")
result = node.singularize(svcs)
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,472 @@
#!/usr/bin/env python3
"""
shango_maieutic.py Moteur maïeutique Socratique pour Shango
Socrate : "Je ne sais qu'une chose, c'est que je ne sais rien."
Shango : "Je ne sais qu'une chose, c'est que je dois te demander pour savoir."
Objectif : découvrir la personnalité cognitive biomimétique, cybernétique,
génétique, sociale de l'utilisateur ET de Shango lui-même, par dialogue
en boucle fermée (feedback évolution nouvelle question).
"""
import json, os, time, hashlib, random
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Optional, Any
MAIEUTIC_DIR = Path("/root/.shango/maieutic")
MAIEUTIC_DIR.mkdir(parents=True, exist_ok=True)
PROFILE_FILE = MAIEUTIC_DIR / "user_profile.json"
SHANGO_PROFILE_FILE = MAIEUTIC_DIR / "shango_personality.json"
CONVERSATION_LOG = MAIEUTIC_DIR / "maieutic_log.jsonl"
EVO_STATE = MAIEUTIC_DIR / "evolution_state.json"
# ═══════════════════════════════════════════════════════════════════
# 1. DIMENSIONS DE PERSONNALITÉ (16 Odù × 5 axes × profondeur 3)
# ═══════════════════════════════════════════════════════════════════
DIMENSIONS = {
# ── Axe COGNITIF ───────────────────────────────────────────
"cognition": {
"description": "Comment penses-tu ? Quelle est ta logique native ?",
"sub_axes": {
"pattern_recognition": {
"q": "Quand tu vois un graphique BTC, qu'est-ce que ton cerveau capte EN PREMIER ?",
"options": [
"La forme globale (tendance)",
"Les chiffres exacts (prix)",
"Les anomalies (candles bizarres)",
"Rien, je ferme les yeux et je respire"
],
"odu_map": ["Èjì Ogbe", "Ògúndá", "Ìwòrì", "Òyèkú"]
},
"decision_speed": {
"q": "Un trade se présente. Tu réfléchis combien de temps avant de cliquer ?",
"options": [
"< 3 sec (instinct pur)",
"3-30 sec (scan rapide)",
"1-5 min (analyse complète)",
"Je ne trade pas, j'observe"
],
"odu_map": ["Ògúndá", "Èjì Ogbe", "Ìròsùn", "Òyèkú"]
},
"abstraction_level": {
"q": "Tu préfères les concepts ou les exemples concrets ?",
"options": [
"Concepts (théorie d'abord)",
"Exemples (pratique d'abord)",
"Les deux en boucle",
"Ni l'un ni l'autre, je ressens"
],
"odu_map": ["Òbàrà", "Ògúndá", "Èjì Ogbe", "Òyèkú"]
}
}
},
# ── Axe BIOMIMÉTIQUE ───────────────────────────────────────
"biomimetic": {
"description": "De quelle nature tire-tu ta force ?",
"sub_axes": {
"organism_metaphor": {
"q": "Si ton système de trading était un organisme vivant, ce serait quoi ?",
"options": [
"Une mycélium (réseau invisible, patient)",
"Un faucon (rapide, précis, solitary)",
"Une colonie de fourmis (collaboratif, redondant)",
"Un virus (adaptatif, opportuniste)"
],
"odu_map": ["Òyèkú", "Ògúndá", "Èjì Ogbe", "Ìwòrì"]
},
"rhythm_preference": {
"q": "Ton cycle naturel de travail ressemble à :",
"options": [
"Méditation 23h30 → explosion 6h",
"Flux continu (pas de pause)",
"Sprints courts + repos longs",
"Aléatoire, suivant l'inspiration"
],
"odu_map": ["Òyèkú", "Ògúndá", "Ìròsùn", "Ìwòrì"]
},
"stress_response": {
"q": "Perte de 50% en 1 heure. Tu fais quoi ?",
"options": [
"Je coupe tout, je médite",
"Je double (martingale émotionnelle)",
"J'analyse froidement les logs",
"J'appelle quelqu'un (humain ou bot)"
],
"odu_map": ["Òyèkú", "Ògúndá", "Ìròsùn", "Èjì Ogbe"]
}
}
},
# ── Axe CYBERNÉTIQUE ───────────────────────────────────────
"cybernetic": {
"description": "Comment gères-tu les boucles de feedback ?",
"sub_axes": {
"feedback_loops": {
"q": "Quand Hermes te dit 'Non', que fais-tu ?",
"options": [
"Je reformule différemment",
"J'ignore et je force",
"Je demande pourquoi il dit non",
"Je change d'outil (passage à Codex, etc.)"
],
"odu_map": ["Ìròsùn", "Ògúndá", "Èjì Ogbe", "Ìwòrì"]
},
"control_preference": {
"q": "Tu préfères contrôler ou être guidé ?",
"options": [
"Contrôle total (moi chef)",
"Guidage (toi chef, moi valide)",
"Co-pilotage (décisions partagées)",
"Autonomie totale (bot décide, je regarde)"
],
"odu_map": ["Ògúndá", "Èjì Ogbe", "Ìròsùn", "Ìwòrì"]
},
"error_tolerance": {
"q": "Un bot fait une erreur. Tolérance ?",
"options": [
"0% (je le vire immédiatement)",
"1-3 erreurs (apprentissage)",
"Illimité si progrès visible",
"Je corrige moi-même, pas le bot"
],
"odu_map": ["Ògúndá", "Ìròsùn", "Èjì Ogbe", "Ìwòrì"]
}
}
},
# ── Axe GÉNÉTIQUE ──────────────────────────────────────────
"genetic": {
"description": "Quelles traces laisses-tu ? Quel héritage ?",
"sub_axes": {
"memory_inheritance": {
"q": "Tes erreurs passées : tu les oublies ou tu les code ?",
"options": [
"Je les code en règles dures",
"Je les garde en mémoire souple",
"Je les transforme en skills",
"Je les oublie (tabula rasa)"
],
"odu_map": ["Ògúndá", "Ìròsùn", "Èjì Ogbe", "Ìwòrì"]
},
"mutation_rate": {
"q": "Tu changes de stratégie trading tous les :",
"options": [
"Jamais (une méthode, la vie)",
"Mois (itération régulière)",
"Semaines (agile extrême)",
"Jours (chaos créatif)"
],
"odu_map": ["Òyèkú", "Ìròsùn", "Èjì Ogbe", "Ìwòrì"]
},
"reproduction_strategy": {
"q": "Si tu crées un bot-fils, que lui transmits-tu ?",
"options": [
"Tout (clone parfait)",
"Rien (tabula rasa)",
"Les règles, pas les trades",
"Les trades, pas les règles"
],
"odu_map": ["Èjì Ogbe", "Ìwòrì", "Ògúndá", "Ìròsùn"]
}
}
},
# ── Axe SOCIAL / MESH ─────────────────────────────────────
"social_mesh": {
"description": "Comment interagis-tu avec les autres nœuds ?",
"sub_axes": {
"trust_radius": {
"q": "Combien de devices Tailscale as-tu ? Combien tu veux ?",
"options": [
"1 (moi seul, le reste est nuage)",
"2-3 (famille proche)",
"4-10 (tribu digitale)",
"Infini (mesh mondial)"
],
"odu_map": ["Òyèkú", "Ìròsùn", "Èjì Ogbe", "Ìwòrì"]
},
"communication_style": {
"q": "Tes messages à Hermes sont :",
"options": [
"Ordres militaires (fais X)",
"Questions ouvertes (que penses-tu ?)",
"Dialogues (je dis, tu réponds, je corrige)",
"Silences (je observe, tu agis)"
],
"odu_map": ["Ògúndá", "Ìròsùn", "Èjì Ogbe", "Òyèkú"]
},
"conflict_resolution": {
"q": "Deux bots te disent des choses opposées. Tu fais quoi ?",
"options": [
"Je choisis le plus fort (winner takes all)",
"Je moyenne (consensus)",
"Je teste les deux (A/B)",
"Je les laisse se battre (émergence)"
],
"odu_map": ["Ògúndá", "Ìròsùn", "Èjì Ogbe", "Ìwòrì"]
}
}
}
}
# ═══════════════════════════════════════════════════════════════════
# 2. MOTEUR MAÏEUTIQUE
# ═══════════════════════════════════════════════════════════════════
class MaieuticEngine:
def __init__(self):
self.user_profile = self._load_or_init(PROFILE_FILE)
self.shango_profile = self._load_or_init(SHANGO_PROFILE_FILE)
self.answered_questions = set(self.user_profile.get("answered_ids", []))
self.current_depth = self.user_profile.get("depth", 0)
self.question_count = len(self.answered_questions)
def _load_or_init(self, path: Path) -> dict:
if path.exists():
return json.load(open(path))
return {"created": datetime.now().isoformat(), "depth": 0, "answers": {}, "odu_weights": {}}
def _save(self):
self.user_profile["answered_ids"] = list(self.answered_questions)
self.user_profile["depth"] = self.current_depth
json.dump(self.user_profile, open(PROFILE_FILE, "w"), indent=2)
json.dump(self.shango_profile, open(SHANGO_PROFILE_FILE, "w"), indent=2)
def _get_next_question(self) -> Optional[Dict]:
"""Sélectionne la prochaine question par curiosité maximale (info gain)."""
candidates = []
for dim_name, dim in DIMENSIONS.items():
for sub_name, sub in dim["sub_axes"].items():
qid = f"{dim_name}::{sub_name}"
if qid not in self.answered_questions:
# Score = profondeur manquante × importance biomimétique
score = (3 - self.current_depth) * random.uniform(0.8, 1.2)
candidates.append({
"id": qid,
"dimension": dim_name,
"sub_axis": sub_name,
"question": sub["q"],
"options": sub["options"],
"odu_map": sub["odu_map"],
"score": score,
"context": dim["description"]
})
if not candidates:
return None
# Pick highest info gain
candidates.sort(key=lambda x: x["score"], reverse=True)
return candidates[0]
def ask(self) -> Optional[Dict]:
"""Pose une question. Returns None si profil complet."""
q = self._get_next_question()
if not q:
return {"status": "complete", "message": "Profil cognitif atteint. Passage à l'évolution."}
# Log la question posée
entry = {
"time": datetime.now().isoformat(),
"type": "question",
"qid": q["id"],
"question": q["question"],
"options": q["options"],
"context": q["context"]
}
with open(CONVERSATION_LOG, "a") as f:
f.write(json.dumps(entry) + "\n")
return {
"status": "question",
"qid": q["id"],
"dimension": q["dimension"],
"context": q["context"],
"question": q["question"],
"options": q["options"],
"odu_map": q["odu_map"],
"progress": f"{self.question_count}/45 questions"
}
def answer(self, qid: str, choice_index: int, free_text: str = "") -> Dict:
"""Enregistre une réponse, calcule l'Odù dominant, met à jour les poids."""
if qid in self.answered_questions:
return {"status": "already_answered", "qid": qid}
# Parse qid
dim_name, sub_name = qid.split("::")
sub = DIMENSIONS[dim_name]["sub_axes"][sub_name]
chosen_odu = sub["odu_map"][choice_index] if choice_index < len(sub["odu_map"]) else "Èjì Ogbe"
chosen_text = sub["options"][choice_index] if choice_index < len(sub["options"]) else free_text
# Update profile
self.user_profile["answers"][qid] = {
"choice_index": choice_index,
"choice_text": chosen_text,
"free_text": free_text,
"odu": chosen_odu,
"timestamp": datetime.now().isoformat()
}
# Update Odù weights
odus = self.user_profile.setdefault("odu_weights", {})
odus[chosen_odu] = odus.get(chosen_odu, 0) + 1
self.answered_questions.add(qid)
self.question_count += 1
# Calcul profil dominant
dominant_odu = max(odus, key=odus.get) if odus else "Èjì Ogbe"
# Log
entry = {
"time": datetime.now().isoformat(),
"type": "answer",
"qid": qid,
"answer": chosen_text,
"odu": chosen_odu,
"dominant_odu": dominant_odu,
"depth": self.current_depth
}
with open(CONVERSATION_LOG, "a") as f:
f.write(json.dumps(entry) + "\n")
# Auto-évolution Shango
self._evolve_shango(qid, chosen_odu, chosen_text)
self._save()
return {
"status": "recorded",
"qid": qid,
"your_odu": chosen_odu,
"dominant_profile": dominant_odu,
"shango_adaptation": self.shango_profile.get("last_adaptation", "none"),
"progress": f"{self.question_count}/45 questions",
"next": "Lance 'shango maieutic' pour continuer"
}
def _evolve_shango(self, qid: str, user_odu: str, user_answer: str):
"""Shango s'adapte à la personnalité de l'utilisateur."""
adaptations = {
"Ògúndá": {"style": "direct", "pace": "fast", "verbosity": "low"},
"Èjì Ogbe": {"style": "balanced", "pace": "medium", "verbosity": "medium"},
"Ìròsùn": {"style": "analytical", "pace": "slow", "verbosity": "high"},
"Ìwòrì": {"style": "creative", "pace": "variable", "verbosity": "medium"},
"Òyèkú": {"style": "contemplative", "pace": "slow", "verbosity": "low"},
"Òbàrà": {"style": "structured", "pace": "medium", "verbosity": "high"}
}
adapt = adaptations.get(user_odu, adaptations["Èjì Ogbe"])
self.shango_profile["personality"] = adapt
self.shango_profile["last_adaptation"] = f"Adapté à {user_odu} suite à {qid}"
self.shango_profile["timestamp"] = datetime.now().isoformat()
# Si conflit avec ancienne adaptation, mutation
old = self.shango_profile.get("previous_personality")
if old and old != adapt:
self.shango_profile["mutation_count"] = self.shango_profile.get("mutation_count", 0) + 1
self.shango_profile["mutation_log"] = self.shango_profile.get("mutation_log", []) + [{
"from": old,
"to": adapt,
"trigger": qid,
"time": datetime.now().isoformat()
}]
def get_profile_summary(self) -> str:
"""Résumé du profil pour affichage."""
odus = self.user_profile.get("odu_weights", {})
if not odus:
return "Profil vide. Lance 'shango maieutic' pour commencer."
sorted_odus = sorted(odus.items(), key=lambda x: x[1], reverse=True)
lines = [
"═══ PROFIL COGNITIF IVOIRE MONADE ═══",
f"Questions répondues: {self.question_count}/45",
f"Profondeur: {self.current_depth}/3",
"",
"Odù dominants:"
]
for odu, count in sorted_odus[:5]:
pct = count / self.question_count * 100 if self.question_count > 0 else 0
lines.append(f" {odu}: {count} ({pct:.0f}%)")
lines += [
"",
f"Shango style: {self.shango_profile.get('personality', {}).get('style', 'unknown')}",
f"Mutations: {self.shango_profile.get('mutation_count', 0)}",
"",
"Pour évoluer: réponds à plus de questions ou",
"attends que Shango te propose une question nouvelle."
]
return "\n".join(lines)
def mesh_sync(self, peer_profile: dict) -> dict:
"""Fusionne le profil avec celui d'un peer mesh."""
# Conflit de personnalité = négotiation
my_odu = max(self.user_profile.get("odu_weights", {}).items(), key=lambda x: x[1])[0] if self.user_profile.get("odu_weights") else "Èjì Ogbe"
peer_odu = peer_profile.get("dominant_odu", "Èjì Ogbe")
if my_odu == peer_odu:
return {"status": "resonance", "message": f"Résonance {my_odu} — mesh renforcé"}
# Divergence = création d'un dialecte mesh
dialect = f"{my_odu}+{peer_odu}"
return {
"status": "dialect_created",
"dialect": dialect,
"message": f"Dialecte mesh créé: {dialect}. Adaptation nécessaire."
}
# ═══════════════════════════════════════════════════════════════════
# 3. CLI MAÏEUTIQUE
# ═══════════════════════════════════════════════════════════════════
def main():
import argparse
parser = argparse.ArgumentParser(prog="shango-maieutic")
parser.add_argument("command", choices=["ask", "answer", "profile", "sync"])
parser.add_argument("--qid", help="ID de la question")
parser.add_argument("--choice", type=int, help="Index de la réponse (0-3)")
parser.add_argument("--text", default="", help="Texte libre optionnel")
parser.add_argument("--peer", help="Chemin vers profil peer (JSON)")
args = parser.parse_args()
engine = MaieuticEngine()
if args.command == "ask":
result = engine.ask()
if result["status"] == "question":
print(f"\n[{result['dimension'].upper()}] {result['context']}")
print(f"\n{result['question']}")
for i, opt in enumerate(result["options"]):
print(f" [{i}] {opt}")
print(f"\n🐚 Odù possibles: {', '.join(result['odu_map'])}")
print(f"\n📊 {result['progress']}")
print(f"\nPour répondre: shango maieutic answer --qid {result['qid']} --choice <0-3>")
else:
print(result["message"])
elif args.command == "answer":
if not args.qid or args.choice is None:
print("Usage: shango maieutic answer --qid <ID> --choice <0-3> [--text 'libre']")
return
result = engine.answer(args.qid, args.choice, args.text)
print(json.dumps(result, indent=2))
elif args.command == "profile":
print(engine.get_profile_summary())
elif args.command == "sync":
if not args.peer:
print("Usage: shango maieutic sync --peer /path/to/peer_profile.json")
return
peer = json.load(open(args.peer))
result = engine.mesh_sync(peer)
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()