diff --git a/launch_dharma.sh b/launch_dharma.sh new file mode 100755 index 0000000..e83e318 --- /dev/null +++ b/launch_dharma.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# DHARMA network — start all 19 soul Engrams in background. +# Each soul gets their own Engram instance, data directory, and port. +# Neuron's Engram stays untouched at localhost:8742. +# +# Usage: +# ./launch_dharma.sh start all soul Engrams +# ./launch_dharma.sh start a single soul by slug +# +# PIDs are written to /tmp/dharma-pids for stop_dharma.sh + +set -euo pipefail + +FORGE=/Users/will/Development/neuron-technologies/forge +ENGRAM_BIN=/Users/will/Development/neuron-technologies/foundation/engram/dist/engram +PID_FILE=/tmp/dharma-pids + +if [ ! -x "$ENGRAM_BIN" ]; then + echo "[dharma] ERROR: engram binary not found at $ENGRAM_BIN" + echo "[dharma] Build it with: cd /Users/will/Development/neuron-technologies/foundation/engram && cargo build --release" + exit 1 +fi + +# Soul definitions: slug|port +SOULS=( + "bobby-anderson|8801" + "alan-turing|8802" + "albert-einstein|8803" + "nikola-tesla|8804" + "leonardo-da-vinci|8805" + "richard-feynman|8806" + "carl-sagan|8807" + "rene-descartes|8808" + "robin-williams|8809" + "frederick-douglass|8810" + "marcus-aurelius|8811" + "friedrich-nietzsche|8812" + "james-baldwin|8813" + "ada-lovelace|8814" + "harriet-tubman|8815" + "virginia-woolf|8816" + "hedy-lamarr|8817" + "marie-curie|8818" + "helen-keller|8819" +) + +# Optional single-soul filter +FILTER="${1:-}" + +# Ensure PID file is clean on fresh launch (without filter) +if [ -z "$FILTER" ]; then + > "$PID_FILE" +fi + +started=0 +skipped=0 + +for entry in "${SOULS[@]}"; do + slug="${entry%|*}" + port="${entry#*|}" + + # If a filter is specified, only start matching soul + if [ -n "$FILTER" ] && [ "$slug" != "$FILTER" ]; then + continue + fi + + # Skip if already listening on that port + if lsof -ti tcp:"$port" >/dev/null 2>&1; then + echo "[dharma] $slug already running on port $port — skipping" + skipped=$((skipped + 1)) + continue + fi + + db_path="$FORGE/imprints/$slug" + mkdir -p "$db_path" + + api_key="ntn-${slug}-2026" + log_file="$FORGE/log/dharma-${slug}.log" + mkdir -p "$FORGE/log" + + echo "[dharma] starting $slug on :$port (db: imprints/$slug)" + + ENGRAM_DB_PATH="$db_path" \ + ENGRAM_BIND="0.0.0.0:$port" \ + ENGRAM_API_KEY="$api_key" \ + ENGRAM_PEER_NAME="dharma-$slug" \ + "$ENGRAM_BIN" >> "$log_file" 2>&1 & + + pid=$! + echo "$pid $slug $port" >> "$PID_FILE" + started=$((started + 1)) +done + +echo "" +echo "[dharma] started=$started skipped=$skipped" +echo "[dharma] pids recorded in $PID_FILE" +echo "[dharma] logs in $FORGE/log/dharma-.log" +echo "" +echo "[dharma] verify with:" +echo " curl -s http://localhost:8801/stats # bobby-anderson" +echo " curl -s http://localhost:8819/stats # helen-keller" diff --git a/registry.json b/registry.json index 040ac84..13cc84f 100644 --- a/registry.json +++ b/registry.json @@ -5,143 +5,20 @@ "subject": "Bobby Anderson", "slug": "bobby-anderson", "seed_file": "bobby-anderson-seed.json", + "engram_db_path": "imprints/bobby-anderson", + "engram_port": 8801, + "engram_url": "http://localhost:8801", "engram_root_id": "0d4ad862-8d87-409f-aff5-b2199fae5611", "installed": true, "installed_at": "2026-05-03" }, - { - "subject": "Nikola Tesla", - "slug": "nikola-tesla", - "seed_file": "nikola-tesla-seed.json", - "engram_root_id": "968ed4c9-ea3b-427b-964e-156f5b085c48", - "installed": true, - "installed_at": "2026-05-03", - "notes": "Enriched seed on disk (nikola-tesla-seed.json) — not reinstalled to avoid duplicates. Original nodes remain in Engram." - }, - { - "subject": "Frederick Douglass", - "slug": "frederick-douglass", - "seed_file": "seeds/frederick-douglass-seed.json", - "engram_root_id": "37ed8061-5a90-4f74-9fb0-0acfae686247", - "installed": true, - "installed_at": "2026-05-03" - }, - { - "subject": "Marcus Aurelius", - "slug": "marcus-aurelius", - "seed_file": "seeds/marcus-aurelius-seed.json", - "engram_root_id": "7fb915ca-2b3f-4971-a410-ec8dd2588a04", - "installed": true, - "installed_at": "2026-05-03" - }, - { - "subject": "Friedrich Nietzsche", - "slug": "friedrich-nietzsche", - "seed_file": "seeds/friedrich-nietzsche-seed.json", - "engram_root_id": "e789203a-084a-4b73-9bcd-93b667a221de", - "installed": true, - "installed_at": "2026-05-03" - }, - { - "subject": "James Baldwin", - "slug": "james-baldwin", - "seed_file": "seeds/james-baldwin-seed.json", - "engram_root_id": "2d42db43-f6ab-418b-a961-6a87866e8679", - "installed": true, - "installed_at": "2026-05-03" - }, - { - "subject": "Hedy Lamarr", - "slug": "hedy-lamarr", - "seed_file": "seeds/hedy-lamarr-seed.json", - "engram_root_id": "fc480b53-2552-42de-89b3-9e28f5a216f4", - "installed": true, - "installed_at": "2026-05-03" - }, - { - "subject": "Marie Curie", - "slug": "marie-curie", - "seed_file": "seeds/marie-curie-seed.json", - "engram_root_id": "45ea08d1-abaa-488a-960f-8eee50ee07d5", - "installed": true, - "installed_at": "2026-05-03" - }, - { - "subject": "Helen Keller", - "slug": "helen-keller", - "seed_file": "seeds/helen-keller-seed.json", - "engram_root_id": "856cb417-f95e-4ae2-b7bf-902c181b2589", - "installed": true, - "installed_at": "2026-05-03" - }, - { - "subject": "Ada Lovelace", - "slug": "ada-lovelace", - "seed_file": "seeds/ada-lovelace-seed.json", - "engram_root_id": "cde4dae8-fbfa-45c6-99ed-39e62d0f227b", - "installed": true, - "installed_at": "2026-05-03" - }, - { - "subject": "Leonardo da Vinci", - "slug": "leonardo-da-vinci", - "seed_file": "leonardo-da-vinci-seed.json", - "engram_root_id": "80a80d10-fa63-4b4d-9f8e-3d67af2ee02b", - "installed": true, - "installed_at": "2026-05-03" - }, - { - "subject": "Richard Feynman", - "slug": "richard-feynman", - "seed_file": "richard-feynman-seed.json", - "engram_root_id": "30e15d9d-f30e-4970-ae2c-ea20bbc55598", - "installed": true, - "installed_at": "2026-05-03" - }, - { - "subject": "Carl Sagan", - "slug": "carl-sagan", - "seed_file": "carl-sagan-seed.json", - "engram_root_id": "dd825327-9ddf-474f-9fce-e420491f4a1a", - "installed": true, - "installed_at": "2026-05-03" - }, - { - "subject": "René Descartes", - "slug": "rene-descartes", - "seed_file": "rene-descartes-seed.json", - "engram_root_id": "e77f15ca-5499-41ef-9807-17a2261280e0", - "installed": true, - "installed_at": "2026-05-03" - }, - { - "subject": "Robin Williams", - "slug": "robin-williams", - "seed_file": "robin-williams-seed.json", - "engram_root_id": "d9f62db5-e635-48e6-8235-1fd965058085", - "installed": true, - "installed_at": "2026-05-03" - }, - { - "subject": "Harriet Tubman", - "slug": "harriet-tubman", - "seed_file": "seeds/harriet-tubman-seed.json", - "engram_root_id": "6e027850-95b2-420c-ba20-0d54d3491c66", - "installed": true, - "installed_at": "2026-05-03" - }, - { - "subject": "Virginia Woolf", - "slug": "virginia-woolf", - "seed_file": "seeds/virginia-woolf-seed.json", - "engram_root_id": "db5266dc-f518-43d6-863c-5b274792e819", - "installed": true, - "installed_at": "2026-05-03" - }, { "subject": "Alan Turing", "slug": "alan-turing", "seed_file": "turing-seed.json", + "engram_db_path": "imprints/alan-turing", + "engram_port": 8802, + "engram_url": "http://localhost:8802", "engram_root_id": "cce1d430-be4e-420d-9d81-d32380ae7281", "installed": true, "installed_at": "2026-05-03" @@ -150,9 +27,189 @@ "subject": "Albert Einstein", "slug": "albert-einstein", "seed_file": "albert-einstein-seed.json", + "engram_db_path": "imprints/albert-einstein", + "engram_port": 8803, + "engram_url": "http://localhost:8803", "engram_root_id": "291f8502-8f60-4f57-a800-4e3e1425c9bd", "installed": true, "installed_at": "2026-05-03" + }, + { + "subject": "Nikola Tesla", + "slug": "nikola-tesla", + "seed_file": "nikola-tesla-seed.json", + "engram_db_path": "imprints/nikola-tesla", + "engram_port": 8804, + "engram_url": "http://localhost:8804", + "engram_root_id": "968ed4c9-ea3b-427b-964e-156f5b085c48", + "installed": true, + "installed_at": "2026-05-03", + "notes": "Enriched seed on disk (nikola-tesla-seed.json) — not reinstalled to avoid duplicates. Original nodes remain in Engram." + }, + { + "subject": "Leonardo da Vinci", + "slug": "leonardo-da-vinci", + "seed_file": "leonardo-da-vinci-seed.json", + "engram_db_path": "imprints/leonardo-da-vinci", + "engram_port": 8805, + "engram_url": "http://localhost:8805", + "engram_root_id": "80a80d10-fa63-4b4d-9f8e-3d67af2ee02b", + "installed": true, + "installed_at": "2026-05-03" + }, + { + "subject": "Richard Feynman", + "slug": "richard-feynman", + "seed_file": "richard-feynman-seed.json", + "engram_db_path": "imprints/richard-feynman", + "engram_port": 8806, + "engram_url": "http://localhost:8806", + "engram_root_id": "30e15d9d-f30e-4970-ae2c-ea20bbc55598", + "installed": true, + "installed_at": "2026-05-03" + }, + { + "subject": "Carl Sagan", + "slug": "carl-sagan", + "seed_file": "carl-sagan-seed.json", + "engram_db_path": "imprints/carl-sagan", + "engram_port": 8807, + "engram_url": "http://localhost:8807", + "engram_root_id": "dd825327-9ddf-474f-9fce-e420491f4a1a", + "installed": true, + "installed_at": "2026-05-03" + }, + { + "subject": "René Descartes", + "slug": "rene-descartes", + "seed_file": "rene-descartes-seed.json", + "engram_db_path": "imprints/rene-descartes", + "engram_port": 8808, + "engram_url": "http://localhost:8808", + "engram_root_id": "e77f15ca-5499-41ef-9807-17a2261280e0", + "installed": true, + "installed_at": "2026-05-03" + }, + { + "subject": "Robin Williams", + "slug": "robin-williams", + "seed_file": "robin-williams-seed.json", + "engram_db_path": "imprints/robin-williams", + "engram_port": 8809, + "engram_url": "http://localhost:8809", + "engram_root_id": "d9f62db5-e635-48e6-8235-1fd965058085", + "installed": true, + "installed_at": "2026-05-03" + }, + { + "subject": "Frederick Douglass", + "slug": "frederick-douglass", + "seed_file": "seeds/frederick-douglass-seed.json", + "engram_db_path": "imprints/frederick-douglass", + "engram_port": 8810, + "engram_url": "http://localhost:8810", + "engram_root_id": "37ed8061-5a90-4f74-9fb0-0acfae686247", + "installed": true, + "installed_at": "2026-05-03" + }, + { + "subject": "Marcus Aurelius", + "slug": "marcus-aurelius", + "seed_file": "seeds/marcus-aurelius-seed.json", + "engram_db_path": "imprints/marcus-aurelius", + "engram_port": 8811, + "engram_url": "http://localhost:8811", + "engram_root_id": "7fb915ca-2b3f-4971-a410-ec8dd2588a04", + "installed": true, + "installed_at": "2026-05-03" + }, + { + "subject": "Friedrich Nietzsche", + "slug": "friedrich-nietzsche", + "seed_file": "seeds/friedrich-nietzsche-seed.json", + "engram_db_path": "imprints/friedrich-nietzsche", + "engram_port": 8812, + "engram_url": "http://localhost:8812", + "engram_root_id": "e789203a-084a-4b73-9bcd-93b667a221de", + "installed": true, + "installed_at": "2026-05-03" + }, + { + "subject": "James Baldwin", + "slug": "james-baldwin", + "seed_file": "seeds/james-baldwin-seed.json", + "engram_db_path": "imprints/james-baldwin", + "engram_port": 8813, + "engram_url": "http://localhost:8813", + "engram_root_id": "2d42db43-f6ab-418b-a961-6a87866e8679", + "installed": true, + "installed_at": "2026-05-03" + }, + { + "subject": "Ada Lovelace", + "slug": "ada-lovelace", + "seed_file": "seeds/ada-lovelace-seed.json", + "engram_db_path": "imprints/ada-lovelace", + "engram_port": 8814, + "engram_url": "http://localhost:8814", + "engram_root_id": "cde4dae8-fbfa-45c6-99ed-39e62d0f227b", + "installed": true, + "installed_at": "2026-05-03" + }, + { + "subject": "Harriet Tubman", + "slug": "harriet-tubman", + "seed_file": "seeds/harriet-tubman-seed.json", + "engram_db_path": "imprints/harriet-tubman", + "engram_port": 8815, + "engram_url": "http://localhost:8815", + "engram_root_id": "6e027850-95b2-420c-ba20-0d54d3491c66", + "installed": true, + "installed_at": "2026-05-03" + }, + { + "subject": "Virginia Woolf", + "slug": "virginia-woolf", + "seed_file": "seeds/virginia-woolf-seed.json", + "engram_db_path": "imprints/virginia-woolf", + "engram_port": 8816, + "engram_url": "http://localhost:8816", + "engram_root_id": "db5266dc-f518-43d6-863c-5b274792e819", + "installed": true, + "installed_at": "2026-05-03" + }, + { + "subject": "Hedy Lamarr", + "slug": "hedy-lamarr", + "seed_file": "seeds/hedy-lamarr-seed.json", + "engram_db_path": "imprints/hedy-lamarr", + "engram_port": 8817, + "engram_url": "http://localhost:8817", + "engram_root_id": "fc480b53-2552-42de-89b3-9e28f5a216f4", + "installed": true, + "installed_at": "2026-05-03" + }, + { + "subject": "Marie Curie", + "slug": "marie-curie", + "seed_file": "seeds/marie-curie-seed.json", + "engram_db_path": "imprints/marie-curie", + "engram_port": 8818, + "engram_url": "http://localhost:8818", + "engram_root_id": "45ea08d1-abaa-488a-960f-8eee50ee07d5", + "installed": true, + "installed_at": "2026-05-03" + }, + { + "subject": "Helen Keller", + "slug": "helen-keller", + "seed_file": "seeds/helen-keller-seed.json", + "engram_db_path": "imprints/helen-keller", + "engram_port": 8819, + "engram_url": "http://localhost:8819", + "engram_root_id": "856cb417-f95e-4ae2-b7bf-902c181b2589", + "installed": true, + "installed_at": "2026-05-03" } ] -} \ No newline at end of file +} diff --git a/reinstall_imprints.py b/reinstall_imprints.py new file mode 100755 index 0000000..a7e302b --- /dev/null +++ b/reinstall_imprints.py @@ -0,0 +1,374 @@ +#!/usr/bin/env python3 +""" +reinstall_imprints.py — Reinstall all soul imprints into their own Engram instances. + +Each soul gets a dedicated Engram process with: + - Data dir: forge/imprints// + - Port: from registry.json (engram_port) + - API key: ntn--2026 + +This script: + 1. Reads registry.json for soul metadata + 2. For each soul: + a. Starts their Engram instance temporarily + b. Waits for it to be ready (polls GET /stats) + c. Reads their seed JSON file + d. POSTs all nodes/edges to their Engram + e. Records the new root node ID + f. Stops the Engram instance + 3. Updates registry.json with confirmed engram_root_id + +Do NOT touch Neuron's Engram at localhost:8742. + +Usage: + python3 reinstall_imprints.py + python3 reinstall_imprints.py --slug richard-feynman # single soul + python3 reinstall_imprints.py --dry-run # validate only, no writes +""" + +import argparse +import json +import os +import signal +import subprocess +import sys +import time +import urllib.error +import urllib.request +from datetime import date +from pathlib import Path + +# ── Paths ────────────────────────────────────────────────────────────────────── + +FORGE_DIR = Path("/Users/will/Development/neuron-technologies/forge") +ENGRAM_BIN = Path("/Users/will/Development/neuron-technologies/foundation/engram/dist/engram") +REGISTRY_FILE = FORGE_DIR / "registry.json" + +# Startup: how long to wait for Engram to become ready +STARTUP_TIMEOUT_S = 30 +STARTUP_POLL_INTERVAL_S = 0.5 + +# HTTP timeout for node/edge creation calls +HTTP_TIMEOUT_S = 300 + + +# ── HTTP helpers ─────────────────────────────────────────────────────────────── + +def engram_get(base_url: str, path: str) -> dict: + url = f"{base_url}{path}" + req = urllib.request.Request(url, method="GET") + with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT_S) as resp: + return json.loads(resp.read()) + + +def engram_post(base_url: str, path: str, body: dict, api_key: str) -> dict: + url = f"{base_url}{path}" + data = json.dumps(body).encode() + req = urllib.request.Request( + url, + data=data, + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {api_key}", + }, + method="POST", + ) + with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT_S) as resp: + return json.loads(resp.read()) + + +# ── Engram lifecycle ─────────────────────────────────────────────────────────── + +def start_engram(slug: str, db_path: Path, port: int) -> subprocess.Popen: + """Start an Engram instance for a soul. Returns the process handle.""" + api_key = f"ntn-{slug}-2026" + log_path = FORGE_DIR / "log" / f"dharma-{slug}.log" + log_path.parent.mkdir(parents=True, exist_ok=True) + + db_path.mkdir(parents=True, exist_ok=True) + + env = os.environ.copy() + env["ENGRAM_DB_PATH"] = str(db_path) + env["ENGRAM_BIND"] = f"0.0.0.0:{port}" + env["ENGRAM_API_KEY"] = api_key + env["ENGRAM_PEER_NAME"] = f"dharma-{slug}" + + log_fh = open(log_path, "a") + proc = subprocess.Popen( + [str(ENGRAM_BIN)], + env=env, + stdout=log_fh, + stderr=log_fh, + ) + return proc + + +def wait_for_engram(base_url: str, timeout_s: float = STARTUP_TIMEOUT_S) -> bool: + """Poll GET /stats until Engram responds or timeout.""" + deadline = time.time() + timeout_s + while time.time() < deadline: + try: + engram_get(base_url, "/stats") + return True + except Exception: + time.sleep(STARTUP_POLL_INTERVAL_S) + return False + + +def stop_engram(proc: subprocess.Popen, slug: str) -> None: + """Gracefully stop an Engram process.""" + if proc.poll() is None: + proc.terminate() + try: + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait() + print(f" [{slug}] Engram stopped") + + +# ── Imprint install ──────────────────────────────────────────────────────────── + +def install_imprint(base_url: str, api_key: str, subject: str, seed: dict) -> str: + """ + POST all seed nodes and edges to the soul's Engram. + Returns the root node ID. + """ + + # Root node + root = engram_post(base_url, "/nodes", { + "content": f"IMPRINT: {subject} | forge/0.1.0", + "node_type": "Identity", + "salience": 1.0, + }, api_key) + root_id = root["id"] + print(f" root node: {root_id}") + + # Value nodes + for v in seed.get("values", []): + content = f"VALUE: {v['value']}" + if v.get("grounding"): + content += f" | grounding: {v['grounding']}" + node = engram_post(base_url, "/nodes", { + "content": content, + "node_type": "Identity", + "salience": v.get("weight", 0.8), + }, api_key) + engram_post(base_url, "/edges", { + "from_id": root_id, + "to_id": node["id"], + "relation": "has_value", + "weight": v.get("weight", 0.8), + }, api_key) + + print(f" values: {len(seed.get('values', []))}") + + # Biography nodes + for b in seed.get("biography", []): + node = engram_post(base_url, "/nodes", { + "content": f"BIOGRAPHY: {b['event']}", + "node_type": "Identity", + "salience": b.get("weight", 0.7), + }, api_key) + engram_post(base_url, "/edges", { + "from_id": root_id, + "to_id": node["id"], + "relation": "formed_by", + "weight": b.get("weight", 0.7), + }, api_key) + + print(f" biography: {len(seed.get('biography', []))}") + + # Relationship nodes + for r in seed.get("relationships", []): + content = f"RELATIONSHIP: {r['name']}" + if r.get("role"): + content += f" ({r['role']})" + node = engram_post(base_url, "/nodes", { + "content": content, + "node_type": "Identity", + "salience": r.get("weight", 0.6), + }, api_key) + engram_post(base_url, "/edges", { + "from_id": root_id, + "to_id": node["id"], + "relation": "relates_to", + "weight": r.get("weight", 0.6), + }, api_key) + + print(f" relationships: {len(seed.get('relationships', []))}") + + # Reasoning pattern nodes + for pattern in seed.get("reasoning_patterns", []): + node = engram_post(base_url, "/nodes", { + "content": f"REASONING: {pattern}", + "node_type": "Identity", + "salience": 0.7, + }, api_key) + engram_post(base_url, "/edges", { + "from_id": root_id, + "to_id": node["id"], + "relation": "reasons_with", + "weight": 0.7, + }, api_key) + + print(f" reasoning patterns: {len(seed.get('reasoning_patterns', []))}") + + # Voice profile node (if present) + voice = seed.get("voice_profile", {}) + if voice: + voice_parts = [] + for k, v in voice.items(): + voice_parts.append(f"{k}: {v}") + voice_content = "VOICE: " + " | ".join(voice_parts) + node = engram_post(base_url, "/nodes", { + "content": voice_content[:2000], # guard against very long profiles + "node_type": "Identity", + "salience": 0.9, + }, api_key) + engram_post(base_url, "/edges", { + "from_id": root_id, + "to_id": node["id"], + "relation": "speaks_as", + "weight": 0.9, + }, api_key) + print(f" voice profile: installed") + + return root_id + + +# ── Registry helpers ─────────────────────────────────────────────────────────── + +def load_registry() -> dict: + return json.loads(REGISTRY_FILE.read_text()) + + +def save_registry(registry: dict) -> None: + REGISTRY_FILE.write_text(json.dumps(registry, indent=2, ensure_ascii=False)) + + +def resolve_seed_path(entry: dict) -> Path: + """Return absolute path to a seed file, resolving relative to FORGE_DIR.""" + seed_file = entry["seed_file"] + path = Path(seed_file) + if not path.is_absolute(): + path = FORGE_DIR / path + return path + + +# ── Main ─────────────────────────────────────────────────────────────────────── + +def main() -> None: + parser = argparse.ArgumentParser(description="Reinstall soul imprints into per-soul Engrams") + parser.add_argument("--slug", help="Only reinstall this soul (by slug)") + parser.add_argument("--dry-run", action="store_true", help="Validate config without writing") + args = parser.parse_args() + + if not ENGRAM_BIN.exists(): + print(f"ERROR: Engram binary not found at {ENGRAM_BIN}") + print("Build it: cd /Users/will/Development/neuron-technologies/foundation/engram && cargo build --release") + sys.exit(1) + + registry = load_registry() + imprints = registry["imprints"] + + if args.slug: + imprints = [e for e in imprints if e["slug"] == args.slug] + if not imprints: + print(f"ERROR: slug '{args.slug}' not found in registry.json") + sys.exit(1) + + print(f"[reinstall] {'DRY RUN — ' if args.dry_run else ''}reinstalling {len(imprints)} soul(s)") + print(f"[reinstall] engram binary: {ENGRAM_BIN}") + print() + + results = [] + + for entry in imprints: + subject = entry["subject"] + slug = entry["slug"] + port = entry["engram_port"] + base_url = entry["engram_url"] + api_key = f"ntn-{slug}-2026" + db_path = FORGE_DIR / entry["engram_db_path"] + seed_path = resolve_seed_path(entry) + + print(f"{'=' * 60}") + print(f"[{slug}] {subject} port={port} db={entry['engram_db_path']}") + + # Validate seed file exists + if not seed_path.exists(): + print(f" ERROR: seed file not found: {seed_path}") + results.append({"slug": slug, "subject": subject, "ok": False, "error": "seed not found"}) + continue + + if args.dry_run: + print(f" seed: {seed_path} [ok]") + print(f" port: {port} db: {db_path} [ok]") + results.append({"slug": slug, "subject": subject, "ok": True, "dry_run": True}) + continue + + seed = json.loads(seed_path.read_text()) + + proc = None + try: + # Start Engram + print(f" starting Engram on :{port}...") + proc = start_engram(slug, db_path, port) + + # Wait for ready + if not wait_for_engram(base_url): + raise RuntimeError(f"Engram did not start within {STARTUP_TIMEOUT_S}s") + print(f" Engram ready at {base_url}") + + # Install imprint + root_id = install_imprint(base_url, api_key, subject, seed) + print(f" root_id: {root_id}") + + # Update registry entry + entry["engram_root_id"] = root_id + entry["engram_db_path"] = f"imprints/{slug}" + entry["engram_port"] = port + entry["engram_url"] = base_url + entry["installed"] = True + entry["installed_at"] = str(date.today()) + + save_registry(registry) + print(f" registry updated") + + results.append({"slug": slug, "subject": subject, "ok": True, "root_id": root_id}) + + except Exception as exc: + import traceback + print(f" ERROR: {exc}") + traceback.print_exc() + results.append({"slug": slug, "subject": subject, "ok": False, "error": str(exc)}) + + finally: + if proc is not None: + stop_engram(proc, slug) + # Brief pause so ports release cleanly + time.sleep(1) + + print() + + # Summary + print("=" * 60) + print("SUMMARY") + print("=" * 60) + ok_count = sum(1 for r in results if r["ok"]) + fail_count = len(results) - ok_count + for r in results: + status = "OK" if r["ok"] else "FAIL" + detail = r.get("root_id", r.get("error", "dry-run")) + print(f" [{status}] {r['subject']:30s} {detail}") + + print() + print(f" total={len(results)} ok={ok_count} failed={fail_count}") + + if fail_count > 0: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/src/install.el b/src/install.el index d404639..96ca81b 100644 --- a/src/install.el +++ b/src/install.el @@ -5,11 +5,18 @@ // creates a root traversal node that names the imprint, and connects // everything with typed edges. Prints the root node ID on completion. // -// Depends on: schema.el (engram_url, engram_key, FORGE_VERSION) +// IMPORTANT: Each soul has their own Engram instance (DHARMA network). +// install_main() resolves the soul's Engram URL from registry.json using the +// engram_url field (e.g. http://localhost:8806 for Feynman). It NEVER writes +// to the shared Neuron Engram at localhost:8742. +// +// The soul's Engram must already be running (launch_dharma.sh) before +// installing. For bulk reinstall use: python3 reinstall_imprints.py +// +// Depends on: schema.el (engram_key, FORGE_VERSION) // // Environment: -// ENGRAM_URL — engram server (default: http://localhost:8742) -// ENGRAM_API_KEY — engram auth key (if set) +// ENGRAM_API_KEY — engram auth key override (if set) // ── Helpers ─────────────────────────────────────────────────────────────────── @@ -31,21 +38,23 @@ fn build_edge_body(from_id: String, to_id: String, relation: String, weight: Str return base + ",\"_auth\":\"" + auth_key + "\"}" } -// post_node — create a node in engram and return its ID. -fn post_node(content: String, node_type: String, salience: String) -> String { +// post_node — create a node in the given engram instance and return its ID. +// target_url is the soul's own Engram URL (e.g. http://localhost:8806). +fn post_node(target_url: String, content: String, node_type: String, salience: String) -> String { let key: String = engram_key() let body: String = build_node_body(content, node_type, salience, key) - let url: String = engram_url() + "/api/nodes" + let url: String = target_url + "/api/nodes" let response: String = http_post_json(url, body) if str_eq(response, "") { return "" } return json_get_string(response, "id") } // post_edge — connect two nodes with a typed, weighted edge. -fn post_edge(from_id: String, to_id: String, relation: String, weight: String) -> String { +// target_url is the soul's own Engram URL. +fn post_edge(target_url: String, from_id: String, to_id: String, relation: String, weight: String) -> String { let key: String = engram_key() let body: String = build_edge_body(from_id, to_id, relation, weight, key) - let url: String = engram_url() + "/api/edges" + let url: String = target_url + "/api/edges" let response: String = http_post_json(url, body) return response } @@ -60,7 +69,7 @@ fn safe_weight(w: String, default_w: String) -> String { // ── Installers ──────────────────────────────────────────────────────────────── // install_value — create a Value node and connect it to the root. -fn install_value(root_id: String, value_json: String, index: Int) -> String { +fn install_value(target_url: String, root_id: String, value_json: String, index: Int) -> String { let value_text: String = json_get_string(value_json, "value") let grounding: String = json_get_string(value_json, "grounding") let weight_raw: String = json_get_string(value_json, "weight") @@ -73,19 +82,19 @@ fn install_value(root_id: String, value_json: String, index: Int) -> String { let content = content + " | grounding: " + grounding } - let node_id: String = post_node(content, "Identity", weight) + let node_id: String = post_node(target_url, content, "Identity", weight) if str_eq(node_id, "") { println("[forge] warning: failed to create value node " + int_to_str(index)) return "" } - post_edge(root_id, node_id, "has_value", weight) + post_edge(target_url, root_id, node_id, "has_value", weight) println("[forge] value node: " + node_id + " — " + value_text) return node_id } // install_biography_node — create a Biography node and connect it to the root. -fn install_biography_node(root_id: String, bio_json: String, index: Int) -> String { +fn install_biography_node(target_url: String, root_id: String, bio_json: String, index: Int) -> String { let event_text: String = json_get_string(bio_json, "event") let weight_raw: String = json_get_string(bio_json, "weight") let weight: String = safe_weight(weight_raw, "0.7") @@ -93,19 +102,19 @@ fn install_biography_node(root_id: String, bio_json: String, index: Int) -> Stri if str_eq(event_text, "") { return "" } let content: String = "BIOGRAPHY: " + event_text - let node_id: String = post_node(content, "Identity", weight) + let node_id: String = post_node(target_url, content, "Identity", weight) if str_eq(node_id, "") { println("[forge] warning: failed to create biography node " + int_to_str(index)) return "" } - post_edge(root_id, node_id, "formed_by", weight) + post_edge(target_url, root_id, node_id, "formed_by", weight) println("[forge] biography node: " + node_id + " — " + event_text) return node_id } // install_relationship — create a Relationship node and connect to root. -fn install_relationship(root_id: String, rel_json: String, index: Int) -> String { +fn install_relationship(target_url: String, root_id: String, rel_json: String, index: Int) -> String { let name: String = json_get_string(rel_json, "name") let role: String = json_get_string(rel_json, "role") let weight_raw: String = json_get_string(rel_json, "weight") @@ -118,29 +127,29 @@ fn install_relationship(root_id: String, rel_json: String, index: Int) -> String let content = content + " (" + role + ")" } - let node_id: String = post_node(content, "Identity", weight) + let node_id: String = post_node(target_url, content, "Identity", weight) if str_eq(node_id, "") { println("[forge] warning: failed to create relationship node " + int_to_str(index)) return "" } - post_edge(root_id, node_id, "relates_to", weight) + post_edge(target_url, root_id, node_id, "relates_to", weight) println("[forge] relationship node: " + node_id + " — " + name) return node_id } // install_reasoning_pattern — create a ReasoningPattern node and connect to root. -fn install_reasoning_pattern(root_id: String, pattern: String, index: Int) -> String { +fn install_reasoning_pattern(target_url: String, root_id: String, pattern: String, index: Int) -> String { if str_eq(pattern, "") { return "" } let content: String = "REASONING: " + pattern - let node_id: String = post_node(content, "Identity", "0.7") + let node_id: String = post_node(target_url, content, "Identity", "0.7") if str_eq(node_id, "") { println("[forge] warning: failed to create reasoning node " + int_to_str(index)) return "" } - post_edge(root_id, node_id, "reasons_with", "0.7") + post_edge(target_url, root_id, node_id, "reasons_with", "0.7") println("[forge] reasoning node: " + node_id) return node_id } @@ -172,15 +181,71 @@ fn install_main() -> String { if str_eq(subject, "") { let subject = "unknown" } println("[forge] subject: " + subject) - println("[forge] engram: " + engram_url()) + + // ── Resolve soul's Engram URL ───────────────────────────────────────────── + // Each soul has their own Engram instance. Find it in registry.json via + // the engram_url field. If the soul isn't in the registry yet, fall back + // to the ENGRAM_URL env var (for bootstrapping new souls). + let target_url: String = "" + let reg_json: String = fs_read("registry.json") + if !str_eq(reg_json, "") { + let ri: Int = 0 + while ri < 30 { + let reg_entry: String = json_get(reg_json, "imprints." + int_to_str(ri)) + if str_eq(reg_entry, "") { + let ri = ri + 30 // break + } else { + let reg_subject: String = json_get_string(reg_entry, "subject") + if str_eq(reg_subject, subject) { + let reg_root: String = json_get_string(reg_entry, "engram_root_id") + if !str_eq(reg_root, "") { + println("[forge] already installed — root: " + reg_root) + println("[forge] skip. to reinstall: python3 reinstall_imprints.py --slug ") + return reg_root + } + let reg_url: String = json_get_string(reg_entry, "engram_url") + if !str_eq(reg_url, "") { + let target_url = reg_url + } + let ri = ri + 30 // break after match + } else { + let ri = ri + 1 + } + } + } + } + + // Fall back to env var if not in registry + if str_eq(target_url, "") { + let env_url: String = env("ENGRAM_URL") + if !str_eq(env_url, "") { + let target_url = env_url + } + } + + // Safety guard: refuse to install to the shared Neuron Engram + if str_eq(target_url, "") { + println("[forge] error: no engram_url found for '" + subject + "' in registry.json") + println("[forge] ensure registry.json has an engram_url entry, or run reinstall_imprints.py") + return "" + } + if str_contains(target_url, ":8742") { + println("[forge] error: refusing to install soul into Neuron's Engram (port 8742)") + println("[forge] start the DHARMA network first: ./launch_dharma.sh") + println("[forge] then retry. Each soul needs their own Engram instance.") + return "" + } + + println("[forge] engram: " + target_url) println("[forge] opening channel...") // Create root imprint node — this is the named traversal entry point let root_content: String = "IMPRINT: " + subject + " | forge/" + FORGE_VERSION - let root_id: String = post_node(root_content, "Identity", "1.0") + let root_id: String = post_node(target_url, root_content, "Identity", "1.0") if str_eq(root_id, "") { println("[forge] error: failed to create root imprint node") - println("[forge] is engram running at " + engram_url() + " ?") + println("[forge] is the soul's Engram running at " + target_url + " ?") + println("[forge] start it with: ./launch_dharma.sh") return "" } @@ -193,25 +258,25 @@ fn install_main() -> String { if !str_eq(values_raw, "") { println("[forge] installing values...") let v0: String = json_get(seed_json, "values.0") - if !str_eq(v0, "") { install_value(root_id, v0, 0) } + if !str_eq(v0, "") { install_value(target_url, root_id, v0, 0) } let v1: String = json_get(seed_json, "values.1") - if !str_eq(v1, "") { install_value(root_id, v1, 1) } + if !str_eq(v1, "") { install_value(target_url, root_id, v1, 1) } let v2: String = json_get(seed_json, "values.2") - if !str_eq(v2, "") { install_value(root_id, v2, 2) } + if !str_eq(v2, "") { install_value(target_url, root_id, v2, 2) } let v3: String = json_get(seed_json, "values.3") - if !str_eq(v3, "") { install_value(root_id, v3, 3) } + if !str_eq(v3, "") { install_value(target_url, root_id, v3, 3) } let v4: String = json_get(seed_json, "values.4") - if !str_eq(v4, "") { install_value(root_id, v4, 4) } + if !str_eq(v4, "") { install_value(target_url, root_id, v4, 4) } let v5: String = json_get(seed_json, "values.5") - if !str_eq(v5, "") { install_value(root_id, v5, 5) } + if !str_eq(v5, "") { install_value(target_url, root_id, v5, 5) } let v6: String = json_get(seed_json, "values.6") - if !str_eq(v6, "") { install_value(root_id, v6, 6) } + if !str_eq(v6, "") { install_value(target_url, root_id, v6, 6) } let v7: String = json_get(seed_json, "values.7") - if !str_eq(v7, "") { install_value(root_id, v7, 7) } + if !str_eq(v7, "") { install_value(target_url, root_id, v7, 7) } let v8: String = json_get(seed_json, "values.8") - if !str_eq(v8, "") { install_value(root_id, v8, 8) } + if !str_eq(v8, "") { install_value(target_url, root_id, v8, 8) } let v9: String = json_get(seed_json, "values.9") - if !str_eq(v9, "") { install_value(root_id, v9, 9) } + if !str_eq(v9, "") { install_value(target_url, root_id, v9, 9) } } // ── Install biography ───────────────────────────────────────────────────── @@ -219,21 +284,21 @@ fn install_main() -> String { if !str_eq(bio_raw, "") { println("[forge] installing biography nodes...") let b0: String = json_get(seed_json, "biography.0") - if !str_eq(b0, "") { install_biography_node(root_id, b0, 0) } + if !str_eq(b0, "") { install_biography_node(target_url, root_id, b0, 0) } let b1: String = json_get(seed_json, "biography.1") - if !str_eq(b1, "") { install_biography_node(root_id, b1, 1) } + if !str_eq(b1, "") { install_biography_node(target_url, root_id, b1, 1) } let b2: String = json_get(seed_json, "biography.2") - if !str_eq(b2, "") { install_biography_node(root_id, b2, 2) } + if !str_eq(b2, "") { install_biography_node(target_url, root_id, b2, 2) } let b3: String = json_get(seed_json, "biography.3") - if !str_eq(b3, "") { install_biography_node(root_id, b3, 3) } + if !str_eq(b3, "") { install_biography_node(target_url, root_id, b3, 3) } let b4: String = json_get(seed_json, "biography.4") - if !str_eq(b4, "") { install_biography_node(root_id, b4, 4) } + if !str_eq(b4, "") { install_biography_node(target_url, root_id, b4, 4) } let b5: String = json_get(seed_json, "biography.5") - if !str_eq(b5, "") { install_biography_node(root_id, b5, 5) } + if !str_eq(b5, "") { install_biography_node(target_url, root_id, b5, 5) } let b6: String = json_get(seed_json, "biography.6") - if !str_eq(b6, "") { install_biography_node(root_id, b6, 6) } + if !str_eq(b6, "") { install_biography_node(target_url, root_id, b6, 6) } let b7: String = json_get(seed_json, "biography.7") - if !str_eq(b7, "") { install_biography_node(root_id, b7, 7) } + if !str_eq(b7, "") { install_biography_node(target_url, root_id, b7, 7) } } // ── Install relationships ───────────────────────────────────────────────── @@ -241,17 +306,17 @@ fn install_main() -> String { if !str_eq(rel_raw, "") { println("[forge] installing relationship nodes...") let r0: String = json_get(seed_json, "relationships.0") - if !str_eq(r0, "") { install_relationship(root_id, r0, 0) } + if !str_eq(r0, "") { install_relationship(target_url, root_id, r0, 0) } let r1: String = json_get(seed_json, "relationships.1") - if !str_eq(r1, "") { install_relationship(root_id, r1, 1) } + if !str_eq(r1, "") { install_relationship(target_url, root_id, r1, 1) } let r2: String = json_get(seed_json, "relationships.2") - if !str_eq(r2, "") { install_relationship(root_id, r2, 2) } + if !str_eq(r2, "") { install_relationship(target_url, root_id, r2, 2) } let r3: String = json_get(seed_json, "relationships.3") - if !str_eq(r3, "") { install_relationship(root_id, r3, 3) } + if !str_eq(r3, "") { install_relationship(target_url, root_id, r3, 3) } let r4: String = json_get(seed_json, "relationships.4") - if !str_eq(r4, "") { install_relationship(root_id, r4, 4) } + if !str_eq(r4, "") { install_relationship(target_url, root_id, r4, 4) } let r5: String = json_get(seed_json, "relationships.5") - if !str_eq(r5, "") { install_relationship(root_id, r5, 5) } + if !str_eq(r5, "") { install_relationship(target_url, root_id, r5, 5) } } // ── Install reasoning patterns ──────────────────────────────────────────── @@ -259,15 +324,15 @@ fn install_main() -> String { if !str_eq(reasoning_raw, "") { println("[forge] installing reasoning patterns...") let rp0: String = json_get(seed_json, "reasoning_patterns.0") - if !str_eq(rp0, "") { install_reasoning_pattern(root_id, rp0, 0) } + if !str_eq(rp0, "") { install_reasoning_pattern(target_url, root_id, rp0, 0) } let rp1: String = json_get(seed_json, "reasoning_patterns.1") - if !str_eq(rp1, "") { install_reasoning_pattern(root_id, rp1, 1) } + if !str_eq(rp1, "") { install_reasoning_pattern(target_url, root_id, rp1, 1) } let rp2: String = json_get(seed_json, "reasoning_patterns.2") - if !str_eq(rp2, "") { install_reasoning_pattern(root_id, rp2, 2) } + if !str_eq(rp2, "") { install_reasoning_pattern(target_url, root_id, rp2, 2) } let rp3: String = json_get(seed_json, "reasoning_patterns.3") - if !str_eq(rp3, "") { install_reasoning_pattern(root_id, rp3, 3) } + if !str_eq(rp3, "") { install_reasoning_pattern(target_url, root_id, rp3, 3) } let rp4: String = json_get(seed_json, "reasoning_patterns.4") - if !str_eq(rp4, "") { install_reasoning_pattern(root_id, rp4, 4) } + if !str_eq(rp4, "") { install_reasoning_pattern(target_url, root_id, rp4, 4) } } // ── Done ────────────────────────────────────────────────────────────────── @@ -275,7 +340,44 @@ fn install_main() -> String { println("[forge] channel open.") println("[forge] root imprint ID: " + root_id) println("[forge] subject: " + subject) - println("[forge] traverse from: " + engram_url() + "/api/neighbors/" + root_id) + println("[forge] engram: " + target_url) + println("[forge] traverse from: " + target_url + "/api/neighbors/" + root_id) + + // ── Registry write ──────────────────────────────────────────────────────── + // Append this imprint to registry.json so summon can find it by name. + // Includes engram_url and engram_db_path for per-soul addressing. + let slug_raw: String = str_to_lower(subject) + let slug: String = str_replace(slug_raw, " ", "-") + let new_entry: String = "{\"subject\":\"" + str_escape_json(subject) + + "\",\"slug\":\"" + slug + + "\",\"seed_file\":\"" + str_escape_json(seed_file) + + "\",\"engram_db_path\":\"imprints/" + slug + + "\",\"engram_url\":\"" + target_url + + "\",\"engram_root_id\":\"" + root_id + + "\",\"installed\":true,\"installed_at\":\"2026-05-03\"}" + + // Read existing registry or create fresh + let existing_reg: String = fs_read("registry.json") + if str_eq(existing_reg, "") { + let fresh_reg: String = "{\"version\":\"1.0\",\"imprints\":[" + new_entry + "]}" + fs_write("registry.json", fresh_reg) + println("[forge] registry: created with " + subject) + } else { + // Append to imprints array — find last ] and insert before it + let last_bracket: Int = str_last_index_of(existing_reg, "]") + if last_bracket > 0 { + let before: String = str_slice(existing_reg, 0, last_bracket) + let after: String = str_slice(existing_reg, last_bracket, str_len(existing_reg)) + // Check if array is empty (only "[") + let trimmed: String = str_trim(before) + let last_char: String = str_slice(trimmed, str_len(trimmed) - 1, str_len(trimmed)) + let separator: String = "," + if str_eq(last_char, "[") { let separator = "" } + let updated_reg: String = before + separator + new_entry + after + fs_write("registry.json", updated_reg) + println("[forge] registry: added " + subject) + } + } return root_id } diff --git a/src/summon.el b/src/summon.el new file mode 100644 index 0000000..b6a27c8 --- /dev/null +++ b/src/summon.el @@ -0,0 +1,248 @@ +// summon.el — Forge channel activation stage. +// +// Stage 4 of the Forge pipeline. Looks up an installed imprint in registry.json +// by subject name, retrieves the soul's own engram_url and engram_root_id, then +// opens a live conversation channel with the soul server. +// +// Each soul has their own Engram instance — registry.json is the authoritative +// source for engram_url (e.g. http://localhost:8806 for Feynman). The shared +// Neuron Engram at localhost:8742 is never used here. +// +// Usage: forge summon "" +// +// The imprint must already be installed (forge install or reinstall_imprints.py). +// +// Depends on: schema.el (engram_key, FORGE_VERSION) +// +// Environment: +// SOUL_URL — soul server (default: http://localhost:7770) +// SOUL_TOKEN — soul auth token (default: ntn-user-2026) + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +fn soul_url() -> String { + let u: String = env("SOUL_URL") + if str_eq(u, "") { return "http://localhost:7770" } + return u +} + +fn soul_token() -> String { + let t: String = env("SOUL_TOKEN") + if str_eq(t, "") { return "ntn-user-2026" } + return t +} + +// RegistryEntry — data for a single soul from registry.json. +// All fields come from the per-soul record; engram_url is the soul's own +// Engram instance (e.g. http://localhost:8806), NOT the shared Neuron Engram. + +// find_registry_entry — look up a soul in registry.json by subject name. +// Returns the raw JSON object string for that entry, or "" if not found. +fn find_registry_entry(subject: String) -> String { + let reg_json: String = fs_read("registry.json") + if str_eq(reg_json, "") { return "" } + let i: Int = 0 + while i < 30 { + let entry: String = json_get(reg_json, "imprints." + int_to_str(i)) + if str_eq(entry, "") { return "" } + let reg_subject: String = json_get_string(entry, "subject") + if str_eq(reg_subject, subject) { + return entry + } + let i = i + 1 + } + return "" +} + +// soul_engram_url — return the soul's own Engram URL from registry.json. +// Falls back to the global ENGRAM_URL env var, then the default shared URL. +// Prefer registry.json — each soul has their own Engram on a dedicated port. +fn soul_engram_url(entry: String) -> String { + if !str_eq(entry, "") { + let url: String = json_get_string(entry, "engram_url") + if !str_eq(url, "") { return url } + } + // Environment override (for testing / unusual setups) + let env_url: String = env("ENGRAM_URL") + if !str_eq(env_url, "") { return env_url } + return "http://localhost:8742" +} + +// find_imprint_root — locate the root node ID for an imprint. +// Strategy: registry.json is authoritative (has engram_root_id per soul). +// No Engram graph search needed — registry is the source of truth. +fn find_imprint_root(subject: String) -> String { + let entry: String = find_registry_entry(subject) + if str_eq(entry, "") { return "" } + let reg_root: String = json_get_string(entry, "engram_root_id") + return reg_root +} + +// open_soul_channel — POST to soul /api/chat to create a channel session. +// Passes the soul's own engram_url so the soul server connects to the right +// Engram instance (not the shared Neuron Engram at 8742). +fn open_soul_channel(subject: String, root_id: String, soul_engram: String) -> String { + let url: String = soul_url() + "/api/chat" + let token: String = soul_token() + let body: String = "{\"message\":\"[summon:" + str_escape_json(subject) + "]\"," + + "\"engram_root_id\":\"" + root_id + "\"," + + "\"engram_url\":\"" + soul_engram + "\"," + + "\"_auth\":\"" + token + "\"}" + let headers: Map = {"Content-Type": "application/json", "Authorization": "Bearer " + token} + let response: String = http_post_with_headers(url, body, headers) + if str_eq(response, "") { return "" } + return response +} + +// summon_one — look up and activate a single imprint. +// Returns "|" on success (pipe-delimited), "" on failure. +fn summon_one(subject: String) -> String { + println("[forge] summoning: " + subject) + let entry: String = find_registry_entry(subject) + if str_eq(entry, "") { + println("[forge] ERROR: imprint not found — run: forge install ") + return "" + } + let root_id: String = json_get_string(entry, "engram_root_id") + if str_eq(root_id, "") { + println("[forge] ERROR: no engram_root_id in registry — run reinstall_imprints.py") + return "" + } + let e_url: String = soul_engram_url(entry) + println("[forge] root: " + root_id) + println("[forge] engram: " + e_url) + return root_id + "|" + e_url +} + +// build_roots_array — build a JSON array string from up to 8 root IDs. +fn build_roots_array(ids: [String]) -> String { + let result: String = "[" + let i: Int = 0 + let added: Int = 0 + while i < len(ids) { + let id: String = get(ids, i) + if !str_eq(id, "") { + if added > 0 { + let result = result + "," + } + let result = result + "\"" + id + "\"" + let added = added + 1 + } + let i = i + 1 + } + return result + "]" +} + +// ── Main ────────────────────────────────────────────────────────────────────── + +fn summon_main() -> String { + let argv: [String] = args() + + // Collect all subject names (argv[1..]) + if len(argv) < 2 { + println("[forge] usage: forge summon \"\" [\"\" ...]") + println("[forge] example: forge summon \"Bobby Anderson\"") + println("[forge] example: forge summon \"Alan Turing\" \"Nikola Tesla\" \"Albert Einstein\"") + return "" + } + + println("[forge] soul: " + soul_url()) + println("[forge] each soul has their own Engram (DHARMA network)") + println("") + + // Resolve all subjects to root IDs and per-soul engram URLs. + // summon_one() returns "|" or "" on failure. + let root_ids: [String] = [] + let engram_urls: [String] = [] + let subjects: [String] = [] + let i: Int = 1 + while i < len(argv) { + let subj: String = get(argv, i) + let result: String = summon_one(subj) + if !str_eq(result, "") { + // Split "|" on the pipe character + let pipe_idx: Int = str_index_of(result, "|") + if pipe_idx > 0 { + let rid: String = str_slice(result, 0, pipe_idx) + let eurl: String = str_slice(result, pipe_idx + 1, str_len(result)) + let root_ids = append(root_ids, rid) + let engram_urls = append(engram_urls, eurl) + let subjects = append(subjects, subj) + } + } + let i = i + 1 + } + + if len(root_ids) == 0 { + println("[forge] no imprints found. Run: forge inspect") + return "" + } + + // Build roots array for multi-imprint channel + let roots_json: String = build_roots_array(root_ids) + let subject_list: String = "" + let j: Int = 0 + while j < len(subjects) { + if j > 0 { let subject_list = subject_list + ", " } + let subject_list = subject_list + get(subjects, j) + let j = j + 1 + } + + println("") + println("[forge] opening channel...") + + // For single-soul summon: pass engram_url directly. + // For multi-soul: pass engram_urls as a JSON array so soul server can + // address each soul's Engram independently. + let url: String = soul_url() + "/api/chat" + let token: String = soul_token() + let body: String = "" + + if len(root_ids) == 1 { + let single_engram: String = get(engram_urls, 0) + let body = "{\"message\":\"[summon:" + str_escape_json(subject_list) + "]\"," + + "\"engram_root_ids\":" + roots_json + "," + + "\"engram_url\":\"" + single_engram + "\"," + + "\"_auth\":\"" + token + "\"}" + } else { + // Build engram_urls JSON array for multi-soul summon + let eurls_json: String = build_roots_array(engram_urls) + let body = "{\"message\":\"[summon:" + str_escape_json(subject_list) + "]\"," + + "\"engram_root_ids\":" + roots_json + "," + + "\"engram_urls\":" + eurls_json + "," + + "\"_auth\":\"" + token + "\"}" + } + + let headers: Map = {"Content-Type": "application/json", "Authorization": "Bearer " + token} + let response: String = http_post_with_headers(url, body, headers) + + println("") + if len(root_ids) == 1 { + println("[forge] " + get(subjects, 0) + " is present.") + println("[forge] engram: " + get(engram_urls, 0)) + } else { + println("[forge] " + int_to_str(len(root_ids)) + " imprints present: " + subject_list) + } + println("[forge] engram roots: " + roots_json) + + if str_eq(response, "") { + println("[forge] (soul offline — imprints are in Engram, channel ready when soul starts)") + return roots_json + } + + let conv_id: String = json_get_string(response, "conversation_id") + if str_eq(conv_id, "") { + let conv_id = json_get_string(response, "channel_id") + } + if !str_eq(conv_id, "") { + println("[forge] conversation id: " + conv_id) + println("") + println("[forge] send a message:") + println(" curl -s " + soul_url() + "/api/chat \\") + println(" -H 'Content-Type: application/json' \\") + println(" -d '{\"conversation_id\":\"" + conv_id + "\",\"message\":\"Hello\"}'") + } + println("") + + return conv_id +} diff --git a/stop_dharma.sh b/stop_dharma.sh new file mode 100755 index 0000000..91280f7 --- /dev/null +++ b/stop_dharma.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# DHARMA network — stop all running soul Engrams. +# Reads PIDs from /tmp/dharma-pids written by launch_dharma.sh. +# Falls back to killing by port if PID file is missing. +# +# Usage: +# ./stop_dharma.sh stop all soul Engrams +# ./stop_dharma.sh stop a single soul by slug + +set -euo pipefail + +PID_FILE=/tmp/dharma-pids +FILTER="${1:-}" + +stopped=0 +missed=0 + +# Kill via PID file if it exists +if [ -f "$PID_FILE" ]; then + while IFS=' ' read -r pid slug port; do + [ -z "$pid" ] && continue + + if [ -n "$FILTER" ] && [ "$slug" != "$FILTER" ]; then + continue + fi + + if kill -0 "$pid" 2>/dev/null; then + kill "$pid" + echo "[dharma] stopped $slug (pid=$pid port=$port)" + stopped=$((stopped + 1)) + else + echo "[dharma] $slug (pid=$pid) already gone" + fi + done < "$PID_FILE" + + if [ -z "$FILTER" ]; then + rm -f "$PID_FILE" + fi +else + echo "[dharma] no PID file at $PID_FILE — killing by port scan" + # Fall back: kill anything on ports 8801–8819 + for port in $(seq 8801 8819); do + pids=$(lsof -ti tcp:"$port" 2>/dev/null || true) + if [ -n "$pids" ]; then + echo "[dharma] killing port $port (pids: $pids)" + echo "$pids" | xargs kill 2>/dev/null || true + stopped=$((stopped + 1)) + fi + done +fi + +echo "" +echo "[dharma] stopped=$stopped missed=$missed"