cyb/cli/src/main.rs

//! cy โ€” the terminal face of a cyb.  (`cyb` is the GUI; `cy` is the CLI.)
//!
//!   cy                         the `cybโ€บ` REPL (interactive)
//!   cy link cat dog            one-shot
//!   cy query                   one-shot query
//!   cy bind <claim>            record a verified mudra migration claim
//!   cy pull <peer.log>         absorb a peer's signals (file anti-entropy)
//!
//! Both modes drive a `cyb_core::Cell`. The REPL is just the interactive mode.
//! Networking rides the stack's own signal frames โ€” a cell's durable log *is*
//! its snapshot of tape frames, so pulling a peer is just absorbing that file.
//! Live transport (radio/QUIC) plugs in above this on the same frames.

use cyb_core::{Cell, MoneyEvent, MoneyWallet, Signal, money_to_sense};
use inf_value::Value;
use std::io::{self, BufRead, IsTerminal, Read, Write};
use std::path::Path;

// โ”€โ”€ color โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// ANSI only when stdout is a terminal โ€” piped output stays clean.

fn tty() -> bool {
    io::stdout().is_terminal()
}

/// Wrap `s` in an SGR code (e.g. "36" for cyan), or return it bare off-tty.
fn paint(code: &str, s: &str) -> String {
    if tty() {
        format!("\x1b[{code}m{s}\x1b[0m")
    } else {
        s.to_string()
    }
}

fn dim(s: &str) -> String {
    paint("90", s)
}
fn cyan(s: &str) -> String {
    paint("36", s)
}
fn green(s: &str) -> String {
    paint("32", s)
}
fn yellow(s: &str) -> String {
    paint("33", s)
}
fn bold(s: &str) -> String {
    paint("1", s)
}
fn red(s: &str) -> String {
    paint("31", s)
}

/// The rainbow ANSI-Shadow wordmark โ€” printed only on a terminal.
const LOGO: &str = "\
\x1b[31m โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•—   โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— \x1b[0m
\x1b[33mโ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ•šโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—\x1b[0m
\x1b[32mโ–ˆโ–ˆโ•‘      โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ• โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•\x1b[0m
\x1b[36mโ–ˆโ–ˆโ•‘       โ•šโ–ˆโ–ˆโ•”โ•  โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—\x1b[0m
\x1b[34mโ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—   โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•\x1b[0m
\x1b[35m โ•šโ•โ•โ•โ•โ•โ•   โ•šโ•โ•   โ•šโ•โ•โ•โ•โ•โ• \x1b[0m";

/// Banner: wordmark + tagline + the field/graph parameters, hemera-style.
fn banner() -> String {
    if !tty() {
        return String::new();
    }
    format!(
        "{LOGO}\n{tag}\n{params}\n",
        tag = paint("37", "    an immortal robot"),
        params = dim("\n    Goldilocks field ยท p = 2^64 - 2^32 + 1\n    \
             cyberlink graph ยท per-neuron signal chains\n    \
             event-sourced ยท never forgets ยท converges to ฯ†*\n"),
    )
}

// โ”€โ”€ rendering โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// A readable particle from a label: its bytes, padded to 32.
fn pid(label: &str) -> [u8; 32] {
    let mut p = [0u8; 32];
    let b = label.as_bytes();
    let n = b.len().min(32);
    p[..n].copy_from_slice(&b[..n]);
    p
}

/// Render a particle id: the label if it's printable-ascii-padded, else a
/// short hash prefix.
fn particle(h: &[u8; 32]) -> String {
    let end = h.iter().position(|&b| b == 0).unwrap_or(32);
    if end > 0 && h[..end].iter().all(u8::is_ascii_graphic) && h[end..].iter().all(|&b| b == 0) {
        String::from_utf8_lossy(&h[..end]).into_owned()
    } else {
        let hex: String = h[..3].iter().map(|b| format!("{b:02x}")).collect();
        format!("{hex}โ€ฆ")
    }
}

/// Render one query cell to a plain string (no color).
fn cell_str(v: &Value) -> String {
    match v {
        Value::Hash(h) => particle(h),
        Value::Int(n) => n.to_string(),
        Value::Bool(b) => b.to_string(),
        Value::Word(w) => w.to_string(),
        Value::Bytes(b) => String::from_utf8_lossy(b).into_owned(),
        Value::Null => "ยท".into(),
        other => format!("{other:?}"),
    }
}

/// Print a query result as an aligned, colored table.
fn print_table(cols: &[String], rows: &[Vec<Value>]) {
    // rendered cell text, per column, to size the columns
    let ncol = cols.len();
    let mut width: Vec<usize> = cols.iter().map(|c| c.chars().count()).collect();
    let text: Vec<Vec<String>> = rows
        .iter()
        .map(|r| r.iter().map(cell_str).collect::<Vec<_>>())
        .collect();
    for row in &text {
        for (i, cell) in row.iter().enumerate().take(ncol) {
            width[i] = width[i].max(cell.chars().count());
        }
    }

    // header
    let header: String = cols
        .iter()
        .enumerate()
        .map(|(i, c)| format!("{:<w$}", c, w = width[i]))
        .collect::<Vec<_>>()
        .join("  ");
    println!("  {}", dim(&header));

    // rows โ€” first column (the particle) cyan, numbers yellow
    for (row, vals) in text.iter().zip(rows) {
        let cells: Vec<String> = row
            .iter()
            .enumerate()
            .take(ncol)
            .map(|(i, cell)| {
                let padded = format!("{:<w$}", cell, w = width[i]);
                match (i, vals.get(i)) {
                    (0, _) => cyan(&padded),
                    (_, Some(Value::Int(_))) => yellow(&padded),
                    _ => padded,
                }
            })
            .collect();
        println!("  {}", cells.join("  "));
    }
    if rows.is_empty() {
        println!("  {}", dim("(empty)"));
    }
}

fn help() {
    let rows = [
        ("id", "this cyb's neuron + pussy address"),
        ("link <a> <b>", "assert a cyberlink a โ†’ b"),
        ("bind <claim>", "record a verified mudra migration claim"),
        ("fund <token> <amt>", "seed balance for tests (genesis)"),
        ("balance [token]", "show balance (default token CYB)"),
        ("send <to> <token> <amt>", "pay coins (grade-2 finality)"),
        (
            "earn <a> <b> [amt]",
            "link aโ†’b + settle Shapley reward (clock B)",
        ),
        ("events", "drain money events (sigma feed)"),
        ("sense", "money events as sense NOTIFY"),
        ("finalize", "advance block height (mature settle rewards)"),
        ("query [inf]", "the graph's nodes (or run a raw inf script)"),
        ("axons", "the graph's edges: from โ†’ to, with weight"),
        (
            "log",
            "the signal log โ€” the event history state derives from",
        ),
        (
            "pull <path>",
            "absorb a peer's signal log (file anti-entropy)",
        ),
        ("state", "how many nodes, axons, and signals the cell holds"),
        (
            "tools",
            "the cyber toolset (hemera, nox, โ€ฆ) โ€” run any by name",
        ),
        (
            "install <tโ€ฆ>",
            "build tools from the registry onto PATH (--all)",
        ),
        ("help ยท quit", "this ยท leave"),
    ];
    let w = rows.iter().map(|(c, _)| c.len()).max().unwrap_or(0);
    println!("{}", dim("commands"));
    for (cmd, desc) in rows {
        println!("  {}   {}", bold(&format!("{cmd:<w$}")), dim(desc));
    }
    let sep = dim(" ยท ");
    println!(
        "\n  {}\n    {}",
        dim("one-shot"),
        [
            green("cy link cat dog"),
            green("cy bind <claim>"),
            green("cy axons"),
            green("cy log")
        ]
        .join(&sep),
    );
}

/// Print the signal log โ€” the event history the graph state is derived from.
/// Each signal shows its particle, the neuron that headed it, its chain step,
/// and the cyberlinks it carries. This is the source of truth; `query` reads
/// the state these signals produce.
fn show_log(cell: &Cell) {
    let sigs = cell.signals();
    if sigs.is_empty() {
        println!("  {}", dim("(no signals yet)"));
        return;
    }
    for s in &sigs {
        let sh: String = s.hash()[..3].iter().map(|b| format!("{b:02x}")).collect();
        print_signal(s, &sh);
    }
    println!("  {}", dim(&format!("{} signal(s)", sigs.len())));
}

/// One signal header + its cyberlinks.
fn print_signal(s: &Signal, short_hash: &str) {
    println!(
        "  {} {}  {} {}  {} {}",
        dim("signal"),
        yellow(&format!("{short_hash}โ€ฆ")),
        dim("neuron"),
        cyan(&particle(&s.neuron)),
        dim("step"),
        yellow(&s.step.to_string()),
    );
    for l in &s.links {
        println!(
            "    {} {} {}",
            cyan(&particle(&l.from)),
            dim("โ†’"),
            cyan(&particle(&l.to))
        );
    }
}

/// The graph's nodes: linked particles and their energy. A node's `particle`
/// column is a readable label when it was linked from one, else a short hash.
fn show_nodes(cell: &Cell) {
    let nodes = cell.nodes();
    if nodes.is_empty() {
        println!("  {}", dim("(no nodes yet)"));
        return;
    }
    let labels: Vec<String> = nodes.iter().map(|(p, _)| particle(p)).collect();
    let w = labels
        .iter()
        .map(|l| l.chars().count())
        .max()
        .unwrap_or(0)
        .max(8);
    println!(
        "  {}  {}",
        dim(&format!("{:<w$}", "particle")),
        dim("energy")
    );
    for ((_, e), label) in nodes.iter().zip(&labels) {
        println!(
            "  {}  {}",
            cyan(&format!("{label:<w$}")),
            yellow(&e.to_string())
        );
    }
}

/// The graph's axons (edges): `from โ†’ to` with the pair's accumulated weight.
fn show_axons(cell: &Cell) {
    let axons = cell.axons();
    if axons.is_empty() {
        println!("  {}", dim("(no axons yet)"));
        return;
    }
    for (from, to, weight) in &axons {
        println!(
            "  {} {} {}  {}",
            cyan(&particle(from)),
            dim("โ†’"),
            cyan(&particle(to)),
            dim(&format!("weight {weight}")),
        );
    }
    println!("  {}", dim(&format!("{} axon(s)", axons.len())));
}

// โ”€โ”€ the cyber toolset โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// The registry (tools.toml, embedded) is the single source of truth: `cy tools`
// lists it, `cy install` builds it, and `cy <name>` dispatches only to it.

#[derive(serde::Deserialize)]
struct Registry {
    tool: Vec<Tool>,
}

/// One entry in the toolset registry.
#[derive(serde::Deserialize)]
struct Tool {
    /// the command name โ€” and the symlink placed on PATH
    name: String,
    /// build directory under `$CYBER_ROOT`
    dir: String,
    /// cargo package for `-p` (a single-crate directory omits it)
    pkg: Option<String>,
    /// built binary name; defaults to `name` when they match
    bin: Option<String>,
    /// short name; also resolves and is linked onto PATH (e.g. `cg` โ†’ cybergraph)
    alias: Option<String>,
    /// one-line description
    desc: String,
}

impl Tool {
    /// The built binary's file name.
    fn bin(&self) -> &str {
        self.bin.as_deref().unwrap_or(&self.name)
    }
}

/// The registry, embedded at build time and parsed once.
fn registry() -> &'static [Tool] {
    static REG: std::sync::OnceLock<Vec<Tool>> = std::sync::OnceLock::new();
    REG.get_or_init(|| {
        toml::from_str::<Registry>(include_str!("../tools.toml"))
            .expect("tools.toml is malformed")
            .tool
    })
}

/// The registered tool of this name, if any. This is the dispatch boundary โ€”
/// only names in the registry run, so `cy ls` is unknown, not `/bin/ls`.
fn tool(name: &str) -> Option<&'static Tool> {
    registry()
        .iter()
        .find(|t| t.name == name || t.alias.as_deref() == Some(name))
}

/// The cyber source root that holds every tool's repo (`$CYBER_ROOT` or `~/cyber`).
fn cyber_root() -> std::path::PathBuf {
    if let Some(r) = std::env::var_os("CYBER_ROOT") {
        return std::path::PathBuf::from(r);
    }
    let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
    std::path::Path::new(&home).join("cyber")
}

/// Where installed tools are symlinked (on PATH).
fn bin_dir() -> std::path::PathBuf {
    let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
    std::path::Path::new(&home).join(".cargo").join("bin")
}

/// Split a command tail into arguments, honoring single and double quotes so a
/// quoted value with spaces (e.g. an inf query script) stays one argument.
fn split_args(s: &str) -> Vec<String> {
    let mut args = Vec::new();
    let mut cur = String::new();
    let mut open = false; // are we mid-token?
    let (mut in_single, mut in_double) = (false, false);
    for c in s.chars() {
        match c {
            '\'' if !in_double => in_single = !in_single,
            '"' if !in_single => in_double = !in_double,
            c if c.is_whitespace() && !in_single && !in_double => {
                if open {
                    args.push(std::mem::take(&mut cur));
                    open = false;
                }
                continue;
            }
            c => cur.push(c),
        }
        open = true;
    }
    if open {
        args.push(cur);
    }
    args
}

/// cy's own builtin verbs โ€” everything else is dispatched to a sibling tool.
const BUILTINS: &[&str] = &[
    "id", "whoami", "link", "query", "axons", "edges", "pull", "bind", "log", "state", "tools",
    "deps", "install", "fund", "balance", "send", "earn", "events", "sense", "finalize", "help",
    "?", "quit", "exit", "q", "",
];

fn is_builtin(cmd: &str) -> bool {
    BUILTINS.contains(&cmd)
}

/// Run a registered tool with an already-split argument list (one-shot: argv is
/// passed verbatim, so quoting is preserved). Callers gate on [`tool`] first, so
/// a `NotFound` here always means "registered but not installed".
fn dispatch_argv(name: &str, args: &[String]) {
    match std::process::Command::new(name).args(args).status() {
        Ok(_) => {}
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
            println!(
                "  {} not installed โ€” {}",
                bold(name),
                green(&format!("cy install {name}"))
            );
        }
        Err(e) => println!("  {}: {name}: {e}", paint("31", "error")),
    }
}

/// Dispatch from the REPL, where the quoted tail must be re-split.
fn dispatch(name: &str, rest: &str) {
    dispatch_argv(name, &split_args(rest));
}

/// List the toolset from the registry, each with its install status.
fn show_tools() {
    println!(
        "{}",
        dim("the cyber toolset โ€” `cy <name> โ€ฆ` runs one ยท `cy install --all` builds them")
    );
    for t in registry() {
        let link = bin_dir().join(&t.name);
        let (mark, note) = if std::fs::metadata(&link).is_ok() {
            (green("โ—"), "") // link resolves to a real binary
        } else if std::fs::symlink_metadata(&link).is_ok() {
            (yellow("โš "), " โ€” stale, run `cy install`") // link exists, target gone
        } else {
            (dim("โ—‹"), "") // not installed
        };
        let alias = t
            .alias
            .as_deref()
            .map(|a| format!("  {}", dim(&format!("({a})"))))
            .unwrap_or_default();
        println!(
            "  {} {}  {}{}{}",
            mark,
            bold(&format!("{:<10}", t.name)),
            dim(&t.desc),
            alias,
            dim(note)
        );
    }
}

/// `cy install [nameโ€ฆ|--all]` โ€” build tools from the registry and link them onto
/// PATH. This is the whole install mechanism; there is no external script.
fn cmd_install(rest: &str) {
    let names = split_args(rest);
    let all = names.is_empty() || names.iter().any(|n| n == "--all" || n == "all");
    let targets: Vec<&Tool> = if all {
        registry().iter().collect()
    } else {
        names
            .iter()
            .filter_map(|n| {
                tool(n).or_else(|| {
                    println!("  {}: not a registered tool (see `cy tools`)", red(n));
                    None
                })
            })
            .collect()
    };
    if targets.is_empty() {
        return;
    }
    let root = cyber_root();
    let _ = std::fs::create_dir_all(bin_dir());
    let (mut ok, mut fail) = (0u32, 0u32);
    for t in targets {
        if install_one(t, &root) {
            ok += 1;
        } else {
            fail += 1;
        }
    }
    let tail = if fail > 0 {
        red(&format!(", {fail} failed"))
    } else {
        String::new()
    };
    println!("  {} {ok} installed{tail}", green("โœ“"));
}

/// Build one tool in release and symlink it onto PATH. Cargo output streams so
/// the build is visible. `$CYBER_ROOT/<dir>` is the workspace; the binary lands
/// at `<dir>/target/release/<bin>`.
fn install_one(t: &Tool, root: &Path) -> bool {
    println!("  {} {}", dim("building"), bold(&t.name));
    let mut cargo = std::process::Command::new("cargo");
    cargo
        .arg("build")
        .arg("--release")
        .current_dir(root.join(&t.dir))
        .env("RUSTC_BOOTSTRAP", "1"); // the workspace pulls crates needing nightly features
    // `pkg` may name several packages (space-separated) โ€” a group like strata
    // builds its dispatcher and every algebra CLI in one invocation.
    if let Some(pkg) = &t.pkg {
        for p in pkg.split_whitespace() {
            cargo.arg("-p").arg(p);
        }
    }
    if !cargo.status().map(|s| s.success()).unwrap_or(false) {
        println!("  {} {} โ€” build failed", red("โœ—"), bold(&t.name));
        return false;
    }
    let target = root.join(&t.dir).join("target/release").join(t.bin());
    // link the name, and its short alias (if any), onto PATH โ€” both point at the
    // one binary, so `cy neural` / `cy neu` and the standalone commands all work.
    if !link_bin(&target, &t.name) {
        return false;
    }
    if let Some(alias) = &t.alias {
        if !link_bin(&target, alias) {
            return false;
        }
    }
    true
}

/// Symlink `<bin_dir>/<name>` โ†’ `target`, replacing any prior link. Reports the link.
fn link_bin(target: &Path, name: &str) -> bool {
    let link = bin_dir().join(name);
    let _ = std::fs::remove_file(&link);
    match std::os::unix::fs::symlink(target, &link) {
        Ok(()) => {
            println!(
                "  {} {} {} {}",
                green("โ—"),
                bold(name),
                dim("โ†’"),
                dim(&link.display().to_string())
            );
            true
        }
        Err(e) => {
            println!("  {}: link {}: {e}", red("error"), name);
            false
        }
    }
}

// โ”€โ”€ commands โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// Absorb a peer's durable log โ€” its concatenated signal frames โ€” into this
/// cell. The grow-only graph converges to the union; re-pulling is a no-op.
fn pull(cell: &mut Cell, peer: &Path) {
    match std::fs::File::open(peer) {
        Ok(mut f) => {
            let mut bytes = Vec::new();
            if f.read_to_end(&mut bytes).is_ok() {
                let n = cell.absorb(&bytes);
                let tag = if n == 0 {
                    dim("already in sync")
                } else {
                    green(&format!("+{n} new"))
                };
                println!("  {} {}  {}", dim("pull"), peer.display(), tag);
            } else {
                println!(
                    "  {}: could not read {}",
                    paint("31", "error"),
                    peer.display()
                );
            }
        }
        Err(e) => println!("  {}: {} โ€” {e}", paint("31", "error"), peer.display()),
    }
}

/// Ingest a mudra migration claim. Verify that the holder controls the legacy
/// Cosmos key, then record the binding `legacy address โ†’ native neuron` as a
/// cyberlink authored by that neuron โ€” migration becomes an immortal graph fact.
fn bind(cell: &mut Cell, claim_str: &str) {
    let Some(c) = mudra::Claim::decode(claim_str) else {
        println!(
            "  {}: malformed claim (expected: address pubkey neuron signature)",
            paint("31", "error")
        );
        return;
    };
    if !mudra::claim::verify(&c, mudra::cosmos::PUSSY) {
        println!("  {} {}", paint("31", "โœ—"), bold("claim does not verify"));
        return;
    }
    // legacy identity as a particle: the 20-byte Cosmos account id, padded
    let mut legacy = [0u8; 32];
    legacy[..20].copy_from_slice(&mudra::cosmos::account_id(&c.pubkey));
    // authored by the migrating neuron itself: it asserts it owns the legacy account
    match cell.link(c.neuron, legacy, c.neuron) {
        Ok(sig) => {
            let s: String = sig[..3].iter().map(|b| format!("{b:02x}")).collect();
            println!(
                "  {} {} {} {}  {}",
                green("โœ“"),
                cyan(&c.address),
                dim("โ†’ neuron"),
                cyan(&particle(&c.neuron)),
                dim(&format!("signal {s}โ€ฆ")),
            );
            println!(
                "  {}",
                dim("migration recorded โ€” the neuron now owns its legacy address")
            );
        }
        Err(e) => println!("  {}: {e:?}", paint("31", "error")),
    }
}

fn print_money_event(e: &MoneyEvent) {
    match e {
        MoneyEvent::TransferOut {
            to,
            amount,
            token,
            signal,
            ..
        } => println!(
            "  {} {} {} {}  {}",
            yellow("out"),
            cyan(&particle(to)),
            yellow(&amount.to_string()),
            dim(&particle(token)),
            dim(&format!("sig {}", &hex3(signal))),
        ),
        MoneyEvent::TransferIn {
            from,
            amount,
            token,
            signal,
            ..
        } => println!(
            "  {} {} {} {}  {}",
            green("in"),
            cyan(&particle(from)),
            yellow(&amount.to_string()),
            dim(&particle(token)),
            dim(&format!("sig {}", &hex3(signal))),
        ),
        MoneyEvent::RewardCredited {
            amount,
            token,
            reason,
            clock,
            ..
        } => println!(
            "  {} {:?} {} {}  {}",
            green("reward"),
            clock,
            yellow(&amount.to_string()),
            dim(&particle(token)),
            dim(&format!("reason {}", &hex3(reason))),
        ),
        MoneyEvent::Finalized { signal, .. } => {
            println!(
                "  {} {}",
                green("final"),
                dim(&format!("sig {}", &hex3(signal)))
            );
        }
        MoneyEvent::TipAdvanced { height, grade4, .. } => println!(
            "  {} h={} grade4={}",
            dim("tip"),
            yellow(&height.to_string()),
            if *grade4 { green("yes") } else { red("no") }
        ),
        MoneyEvent::BalanceUpdated {
            amount,
            token,
            tip_height,
            ..
        } => println!(
            "  {} {} {} @h{}",
            dim("bal"),
            yellow(&amount.to_string()),
            dim(&particle(token)),
            tip_height
        ),
        MoneyEvent::FinalityFailed { reason, .. } => {
            println!("  {} {}", red("fail"), dim(reason));
        }
    }
}

/// Run one command line against the cell. Returns false to stop the REPL.
fn exec(cell: &mut Cell, wallet: &mut MoneyWallet, id: &Id, line: &str) -> bool {
    let mut it = line.trim().splitn(2, ' ');
    match it.next().unwrap_or("") {
        "id" | "whoami" => show_id(id),
        "fund" => {
            let mut ab = it.next().unwrap_or("").split_whitespace();
            match (ab.next(), ab.next()) {
                (Some(tok), Some(amt)) => {
                    let amount: u64 = amt.parse().unwrap_or(0);
                    wallet.fund_for_test(cell, pid(tok), amount);
                    println!(
                        "  {} funded {} {}  tip grade4={}",
                        green("โœ“"),
                        yellow(&amount.to_string()),
                        cyan(tok),
                        if wallet.grade4() {
                            green("yes")
                        } else {
                            red("no")
                        }
                    );
                }
                _ => println!("  {}: fund <token> <amount>", dim("usage")),
            }
        }
        "balance" => {
            let tok = it
                .next()
                .map(str::trim)
                .filter(|s| !s.is_empty())
                .unwrap_or("CYB");
            let token = pid(tok);
            if !wallet.grade4() {
                wallet.sync_tip_local(cell);
            }
            match wallet.open_balance(cell, &id.neuron, &token) {
                Ok((amt, _)) => println!(
                    "  {} {} {}  {}",
                    dim("balance"),
                    yellow(&amt.to_string()),
                    cyan(tok),
                    dim(&format!("(tip h={})", wallet.tip().height)),
                ),
                Err(e) => println!("  {}: {e:?}", paint("31", "error")),
            }
        }
        "send" => {
            let mut ab = it.next().unwrap_or("").split_whitespace();
            match (ab.next(), ab.next(), ab.next()) {
                (Some(to), Some(tok), Some(amt)) => {
                    let amount: u64 = amt.parse().unwrap_or(0);
                    if !wallet.grade4() {
                        wallet.sync_tip_local(cell);
                    }
                    match wallet.send(cell, pid(to), pid(tok), amount) {
                        Ok((sig, ev)) => {
                            println!(
                                "  {} {} {} {} โ†’ {}  {}",
                                green("โœ“"),
                                yellow(&amount.to_string()),
                                cyan(tok),
                                dim("to"),
                                cyan(to),
                                dim(&format!(
                                    "sig {}โ€ฆ final={}",
                                    &hex3(&sig),
                                    ev.verify(wallet.tip())
                                )),
                            );
                        }
                        Err(e) => println!("  {}: {e:?}", paint("31", "error")),
                    }
                }
                _ => println!("  {}: send <to> <token> <amount>", dim("usage")),
            }
        }
        "earn" => {
            // link aโ†’b with stake + live settle โ†’ clock-B reward mint
            let mut ab = it.next().unwrap_or("").split_whitespace();
            match (ab.next(), ab.next()) {
                (Some(a), Some(b)) => {
                    let stake: u64 = ab.next().and_then(|s| s.parse().ok()).unwrap_or(8000);
                    if !wallet.grade4() {
                        wallet.sync_tip_local(cell);
                    }
                    // seed a tiny base graph so impulse/settle has structure
                    if wallet.pending_claims().is_empty() {
                        use tru::Link;
                        let base = vec![
                            Link::stake(pid("x"), pid("y"), 100),
                            Link::stake(pid("y"), pid("z"), 100),
                            Link::stake(pid("z"), pid("x"), 100),
                        ];
                        wallet.set_reward_base(base);
                    }
                    wallet.reward_token = pid("CYB");
                    wallet.reward_budget = 1000;
                    wallet.use_live_epoch = true;
                    wallet.settle_depth = 1;
                    match wallet.link_and_settle(cell, pid(a), pid(b), stake, 1) {
                        Ok((sid, rec, minted)) => {
                            println!(
                                "  {} {} {} {}  stake={}  {}",
                                green("โœ“"),
                                cyan(a),
                                dim("โ†’"),
                                cyan(b),
                                yellow(&stake.to_string()),
                                dim(&format!("sig {}โ€ฆ", &hex3(&sid))),
                            );
                            println!(
                                "  {} minted {} CYB  epoch={}  receipt {}โ€ฆ",
                                green("reward"),
                                yellow(&minted.to_string()),
                                yellow(&rec.epoch.to_string()),
                                dim(&hex3(&rec.receipt_hash)),
                            );
                            println!(
                                "  {} run `finalize` once to mature clock-B (depth={})",
                                dim("tip"),
                                wallet.settle_depth
                            );
                        }
                        Err(e) => println!("  {}: {e:?}", paint("31", "error")),
                    }
                }
                _ => println!("  {}: earn <from> <to> [stake]", dim("usage")),
            }
        }
        "events" => {
            let ev = wallet.drain_events();
            if ev.is_empty() {
                println!("  {}", dim("(no money events)"));
            } else {
                for e in &ev {
                    print_money_event(e);
                }
            }
        }
        "sense" => {
            let ev = wallet.drain_events();
            let notes = money_to_sense(id.neuron, &ev);
            if notes.is_empty() {
                println!("  {}", dim("(no sense notifications)"));
            } else {
                for n in notes {
                    println!(
                        "  {} {} {} {}  {}",
                        green("notify"),
                        cyan(n.kind),
                        yellow(&n.amount.to_string()),
                        dim(&particle(&n.token)),
                        dim(&format!("reason {}", &hex3(&n.reason))),
                    );
                }
            }
        }
        "finalize" => {
            wallet.finalize_block(cell);
            let ready = wallet.mature_settles();
            println!(
                "  {} block  tip h={} grade4={}",
                green("โœ“"),
                yellow(&wallet.tip().height.to_string()),
                if wallet.grade4() {
                    green("yes")
                } else {
                    red("no")
                }
            );
            for (tok, amt, reason) in ready {
                println!(
                    "  {} settle mature {} {}  {}",
                    green("reward"),
                    yellow(&amt.to_string()),
                    dim(&particle(&tok)),
                    dim(&format!("reason {}", &hex3(&reason))),
                );
            }
        }
        "link" => {
            let mut ab = it.next().unwrap_or("").split_whitespace();
            match (ab.next(), ab.next()) {
                (Some(a), Some(b)) => match cell.link(id.neuron, pid(a), pid(b)) {
                    Ok(sig) => {
                        let s: String = sig[..3].iter().map(|b| format!("{b:02x}")).collect();
                        println!(
                            "  {} {} {} {}  {}",
                            green("โœ“"),
                            cyan(a),
                            dim("โ†’"),
                            cyan(b),
                            dim(&format!("signal {s}โ€ฆ")),
                        );
                    }
                    Err(e) => println!("  {}: {e:?}", paint("31", "error")),
                },
                _ => println!("  {}: link <a> <b>", dim("usage")),
            }
        }
        "query" => match it.next() {
            // bare `query` = the graph's nodes; `query <inf>` runs a raw inf script
            None => show_nodes(cell),
            Some(inf) => match cell.query(inf) {
                Ok(out) => print_table(&out.columns, &out.rows),
                Err(e) => println!("  {}: {e:?}", paint("31", "error")),
            },
        },
        "axons" | "edges" => show_axons(cell),
        "pull" => match it.next() {
            Some(path) => pull(cell, Path::new(path.trim())),
            None => println!("  {}: pull <peer-log-path>", dim("usage")),
        },
        "bind" => match it.next() {
            Some(claim) => bind(cell, claim.trim()),
            None => println!("  {}: bind <claim>   (from `mudra claim โ€ฆ`)", dim("usage")),
        },
        "log" => show_log(cell),
        "state" => println!(
            "  {} {} {} {} {} {} {} {}",
            yellow(&cell.nodes().len().to_string()),
            dim("nodes"),
            dim("ยท"),
            yellow(&cell.axons().len().to_string()),
            dim("axons"),
            dim("ยท"),
            yellow(&cell.len().to_string()),
            dim("signals"),
        ),
        "tools" | "deps" => show_tools(),
        "install" => cmd_install(it.next().unwrap_or("")),
        "help" | "?" => help(),
        "quit" | "exit" | "q" => return false,
        "" => {}
        // a registered tool runs; anything else is unknown โ€” cy is not a shell
        other => {
            if tool(other).is_some() {
                dispatch(other, it.next().unwrap_or(""));
            } else {
                println!(
                    "  {}: {other}   {}",
                    dim("unknown"),
                    dim("(try: help ยท tools)")
                );
            }
        }
    }
    true
}

/// Where a cyb keeps its graph by default. `~/cyb` is visible, not
/// dotfile-hidden โ€” nothing about a neuron's own graph needs hiding from
/// its owner.
fn default_path() -> std::path::PathBuf {
    cyb_dir().join("graph.log")
}

fn cyb_dir() -> std::path::PathBuf {
    let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
    std::path::Path::new(&home).join("cyb")
}

/// This cyb's identity: a real neuron, not a placeholder.
struct Id {
    /// the author of every signal this cyb emits: `Hemera(pubkey)`
    neuron: [u8; 32],
    /// the SEC1-compressed secp256k1 public key
    pubkey: [u8; 33],
    /// the matching legacy `pussy1โ€ฆ` account (same key, coin type 118)
    address: String,
}

/// Load this cyb's identity, creating one on first run. The identity is a BIP-39
/// mnemonic stored next to the graph; the neuron is `Hemera(pubkey)` of the key
/// derived at the Cosmos path โ€” the *same* key that owns the matching pussy
/// account, so a neuron can migrate its own legacy identity to itself.
fn identity() -> Id {
    let dir = cyb_dir();
    let _ = std::fs::create_dir_all(&dir);
    let file = dir.join("mnemonic");

    let mnemonic = match std::fs::read_to_string(&file) {
        Ok(m) if !m.trim().is_empty() => m.trim().to_string(),
        _ => {
            let m = mudra::seed::generate_mnemonic().expect("generate mnemonic");
            let _ = std::fs::write(&file, format!("{m}\n"));
            #[cfg(unix)]
            {
                use std::os::unix::fs::PermissionsExt;
                let _ = std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o600));
            }
            eprintln!(
                "  {} new neuron minted ยท {}",
                green("โœฆ"),
                dim(&file.display().to_string())
            );
            eprintln!(
                "  {}",
                dim("back up this mnemonic โ€” it is the only key to this neuron")
            );
            m
        }
    };

    let key = mudra::seed::cosmos_key(&mnemonic, "").expect("derive key from mnemonic");
    let pubkey = mudra::cosmos::compressed(key.verifying_key());
    let neuron = mudra::claim::neuron_of(&pubkey);
    let address = mudra::cosmos::address(&pubkey, mudra::cosmos::PUSSY).unwrap_or_default();
    Id {
        neuron,
        pubkey,
        address,
    }
}

/// Show who this cyb is.
fn show_id(id: &Id) {
    let hx = |b: &[u8]| -> String { b.iter().map(|x| format!("{x:02x}")).collect() };
    println!("  {}  {}", dim("neuron  "), green(&hx(&id.neuron)));
    println!("  {}  {}", dim("pubkey  "), dim(&hx(&id.pubkey)));
    println!("  {}  {}", dim("pussy   "), cyan(&id.address));
}

fn main() {
    let id = identity();
    let path = default_path();
    // durable by default โ€” the graph survives restart
    let mut cell = Cell::open(&path).unwrap_or_else(|_| Cell::ephemeral());
    let mut wallet = MoneyWallet::new(id.neuron).with_tip_prover();
    wallet.sync_tip_local(&cell);

    let args: Vec<String> = std::env::args().skip(1).collect();
    if matches!(
        args.first().map(String::as_str),
        Some("help" | "--help" | "-h")
    ) {
        print!("{}", banner());
        help();
        return;
    }
    if !args.is_empty() {
        let first = args[0].as_str();
        // A tool invocation keeps argv intact (quoting preserved); builtins take
        // the joined line.
        if is_builtin(first) {
            exec(&mut cell, &mut wallet, &id, &args.join(" "));
        } else if tool(first).is_some() {
            dispatch_argv(first, &args[1..]);
        } else {
            println!(
                "  {}: {first}   {}",
                dim("unknown"),
                dim("(try: help ยท tools)")
            );
        }
        return;
    }

    // interactive REPL
    print!("{}", banner());
    println!(
        "  {} {}   {}",
        dim(&path.display().to_string()),
        dim("ยท"),
        dim(&format!(
            "neuron {}โ€ฆ ยท {} nodes ยท type `help`",
            &hex3(&id.neuron),
            cell.nodes().len()
        )),
    );
    let prompt = format!("{}{} ", cyan("cyb"), dim("โ€บ"));
    let stdin = io::stdin();
    loop {
        print!("{prompt}");
        io::stdout().flush().ok();
        let mut line = String::new();
        if stdin.lock().read_line(&mut line).unwrap_or(0) == 0 {
            println!();
            break;
        }
        if !exec(&mut cell, &mut wallet, &id, &line) {
            break;
        }
    }
}

/// Short hex prefix of a particle, for one-line display.
fn hex3(b: &[u8]) -> String {
    b[..3].iter().map(|x| format!("{x:02x}")).collect()
}

Homonyms

cyb/shell/src/main.rs
neural/rune/cli/main.rs
soft3/nox/cli/main.rs
cyb/apps/src/main.rs
soft3/glia/import/main.rs
cyb/optica/src/main.rs
warriors/trisha/cli/main.rs
soft3/tru/cli/main.rs
neural/trident/src/main.rs
soft3/radio/iroh-dns-server/src/main.rs
soft3/bbg/cli/src/main.rs
soft3/hemera/cli/src/main.rs
neural/rs/pure-rust-check/src/main.rs
neural/rs/cli/src/main.rs
soft3/radio/iroh-relay/src/main.rs
neural/rs/link/src/main.rs
neural/rs/macho-linker/src/main.rs
neural/eidos/cli/src/main.rs
soft3/cybergraph/cli/src/main.rs
soft3/glia/run/cli/main.rs
soft3/foculus/src/bin/main.rs
soft3/lens/cli/src/main.rs
soft3/radio/radio-cli/src/main.rs
soft3/radio/particle/src/main.rs
soft3/zheng/cli/src/main.rs
neural/rs/rsc/src/main.rs
cyb/honeycrisp/rane/src/probe/main.rs
soft3/strata/jali/cli/src/main.rs
cyb/wysm/crates/cli/src/main.rs
soft3/strata/genies/cli/src/main.rs
soft3/strata/kuro/cli/src/main.rs
soft3/lytics/rs/agent/src/main.rs
neural/inf/rs/cli/src/main.rs
cyb/honeycrisp/acpu/src/probe/main.rs
bootloader/go-cyber/mcp/rust/src/main.rs
soft3/strata/trop/cli/src/main.rs
soft3/lytics/rs/ingest/src/main.rs
soft3/strata/nebu/cli/src/main.rs
cyb/honeycrisp/unimem/experiments/iosurface_probe/src/main.rs
cyb/honeycrisp/unimem/experiments/hyp_probe/src/main.rs
cyb/honeycrisp/unimem/experiments/dext_contiguous_alloc/client/src/main.rs
cyb/honeycrisp/unimem/experiments/dext_iosurface_pa/client/src/main.rs

Graph