use clap::{CommandFactory, Parser, Subcommand};
use serde::Serialize;
#[derive(Parser)]
#[command(name = "hypha")]
#[command(version)]
#[command(about = "CMN Client - A bio-digital extension for Visitors to release and absorb Spores")]
#[command(after_long_help = concat!(
"All output follows Agent-First Data format:\n",
" {\"code\": \"ok\", \"result\": {...}, \"trace\": {...}}\n",
"\n",
"Quick start (try with cmn.dev):\n",
" hypha sense cmn://cmn.dev\n",
" hypha sense cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2", "\n",
" hypha spawn cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2", "\n",
" hypha cache list",
))]
pub struct Cli {
#[arg(short, long, default_value = "json", global = true)]
pub output: String,
#[arg(long, value_delimiter = ',', global = true)]
pub log: Vec<String>,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand, Serialize)]
#[serde(tag = "command", rename_all = "snake_case")]
pub enum Commands {
#[command(after_long_help = concat!(
"URI types:\n",
" cmn://DOMAIN List all spores on a site\n",
" cmn://DOMAIN/HASH View a specific spore\n",
"\n",
"Examples:\n",
" hypha sense cmn://cmn.dev\n",
" hypha sense cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2", "\n",
" hypha sense cmn://cmn.dev -o yaml",
))]
Sense {
uri: String,
},
#[command(after_long_help = concat!(
"Without --verdict: downloads the spore for local review.\n",
"With --verdict: records a verdict (sweet, fresh, safe, rotten, toxic).\n",
"\n",
"Examples:\n",
" hypha taste cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2", "\n",
" hypha taste cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
" --verdict safe --notes \"Reviewed: clean code\"\n",
" hypha taste cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
" --verdict safe --domain cmn.dev --synapse https://synapse.cmn.dev",
))]
Taste {
uri: String,
#[arg(long, value_name = "VERDICT")]
verdict: Option<substrate::TasteVerdict>,
#[arg(long)]
notes: Option<String>,
#[arg(long)]
synapse: Option<String>,
#[arg(long)]
synapse_token_secret: Option<String>,
#[arg(long)]
domain: Option<String>,
},
#[command(after_long_help = concat!(
"Distribution sources (auto-detected):\n",
" archive Download .tar.zst archive (default, fastest)\n",
" git Clone from dist.git URL if available\n",
"\n",
"Examples:\n",
" hypha spawn cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2", "\n",
" hypha spawn cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
" my-project --vcs git\n",
" hypha spawn cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
" --dist git",
))]
Spawn {
uri: String,
directory: Option<String>,
#[arg(long, value_name = "TYPE")]
vcs: Option<String>,
#[arg(long, value_name = "SOURCE")]
dist: Option<String>,
#[arg(long)]
bond: bool,
},
#[command(after_long_help = "\
Run inside a previously spawned directory to update it.
Uses Synapse lineage to discover newer versions from the same publisher.
If local files have been modified (git dirty or tree hash mismatch),
grow refuses to overwrite them and shows the cache path for manual merge.
Examples:
hypha grow
hypha grow --synapse synapse.cmn.dev
hypha grow --dist git
hypha grow --dist archive
hypha grow --bond --synapse synapse.cmn.dev")]
Grow {
#[arg(long, value_name = "SOURCE")]
dist: Option<String>,
#[arg(long)]
synapse: Option<String>,
#[arg(long)]
synapse_token_secret: Option<String>,
#[arg(long)]
bond: bool,
},
#[command(after_long_help = concat!(
"Absorb downloads spores into .cmn/absorb/ for AI-assisted merge.\n",
"Use --discover to auto-discover descendants via Synapse.\n",
"\n",
"Examples:\n",
" hypha absorb cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2", "\n",
" hypha absorb --discover --synapse https://synapse.cmn.dev\n",
" hypha absorb --discover --synapse https://synapse.cmn.dev --max-depth 5",
))]
Absorb {
#[arg(required_unless_present = "discover")]
uris: Vec<String>,
#[arg(long)]
discover: bool,
#[arg(long)]
synapse: Option<String>,
#[arg(long)]
synapse_token_secret: Option<String>,
#[arg(long, default_value = "10")]
max_depth: u32,
},
#[command(after_long_help = "\
Examples:
hypha bond
hypha bond --status
hypha bond --clean")]
Bond {
#[arg(long)]
clean: bool,
#[arg(long)]
status: bool,
},
#[command(after_long_help = "\
Replicates spores from another domain to yours. The hash stays the same
because core + core_signature are preserved. Only capsule_signature changes.
Examples:
hypha replicate cmn://other.dev/HASH --domain my.dev
hypha replicate --refs --domain my.dev")]
Replicate {
#[arg(required_unless_present = "refs")]
uris: Vec<String>,
#[arg(long)]
refs: bool,
#[arg(long)]
domain: String,
#[arg(long)]
site_path: Option<String>,
},
#[command(after_long_help = "\
Examples:
hypha hatch --id my-tool --name \"My Tool\" --synopsis \"A useful tool\"
hypha hatch --intent \"Provide a reusable HTTP client for CMN agents\" --mutations \"Initial release\"
hypha hatch --license MIT --domain cmn.dev
Subcommands:
hypha hatch bond set/remove/clear Manage bonds in spore.core.json
hypha hatch tree set/show Manage tree configuration")]
#[command(args_conflicts_with_subcommands = true)]
Hatch {
#[arg(long)]
id: Option<String>,
#[arg(long)]
version: Option<String>,
#[arg(long)]
name: Option<String>,
#[arg(long)]
domain: Option<String>,
#[arg(long)]
synopsis: Option<String>,
#[arg(long)]
intent: Vec<String>,
#[arg(long)]
mutations: Vec<String>,
#[arg(long)]
license: Option<String>,
#[command(subcommand)]
#[serde(skip)]
command: Option<HatchCommands>,
},
#[command(after_long_help = "\
Requires `hypha mycelium root` first to set up the site.
Examples:
hypha release --domain cmn.dev
hypha release --domain cmn.dev --source ./my-spore
hypha release --domain cmn.dev --dry-run # pre-compute URI without releasing
hypha release --domain cmn.dev --archive zstd
hypha release --domain cmn.dev --dist-git https://github.com/user/repo --dist-ref v1.0")]
Release {
#[arg(long)]
domain: String,
#[arg(long)]
source: Option<String>,
#[arg(long)]
site_path: Option<String>,
#[arg(long)]
dist_git: Option<String>,
#[arg(long)]
dist_ref: Option<String>,
#[arg(long, value_name = "FORMAT", default_value = "zstd")]
archive: String,
#[arg(long)]
dry_run: bool,
},
#[command(after_long_help = concat!(
"Direction:\n",
" --direction in Find descendants / forks (default)\n",
" --direction out Trace ancestors / spawn chain\n",
"\n",
"Examples:\n",
" hypha lineage cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
" --synapse https://synapse.cmn.dev\n",
" hypha lineage cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
" --direction out --synapse https://synapse.cmn.dev\n",
" hypha lineage cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
" --synapse https://synapse.cmn.dev --max-depth 5",
))]
Lineage {
uri: String,
#[arg(long)]
direction: Option<String>,
#[arg(long)]
synapse: Option<String>,
#[arg(long)]
synapse_token_secret: Option<String>,
#[arg(long, default_value = "10")]
max_depth: u32,
},
#[command(after_long_help = "\
Examples:
hypha search \"protocol spec\" --synapse https://synapse.cmn.dev
hypha search \"data format\" --synapse https://synapse.cmn.dev --domain cmn.dev
hypha search \"agent tools\" --synapse https://synapse.cmn.dev --license MIT --limit 5
hypha search \"http client\" --bonds spawned_from:cmn://cmn.dev/b3.abc123")]
Search {
query: String,
#[arg(long)]
synapse: Option<String>,
#[arg(long)]
synapse_token_secret: Option<String>,
#[arg(long)]
domain: Option<String>,
#[arg(long)]
license: Option<String>,
#[arg(long)]
bonds: Option<String>,
#[arg(long, default_value = "20")]
limit: u32,
},
Mycelium {
#[command(subcommand)]
#[serde(flatten)]
action: MyceliumAction,
},
Synapse {
#[command(subcommand)]
#[serde(flatten)]
action: SynapseAction,
},
Cache {
#[command(subcommand)]
#[serde(flatten)]
action: CacheAction,
},
Config {
#[command(subcommand)]
#[serde(flatten)]
action: ConfigAction,
},
}
#[derive(Subcommand, Serialize)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum HatchCommands {
#[command(after_long_help = "\
Examples:
hypha hatch bond set --uri cmn://cmn.dev/b3.abc --relation follows --id my-lib --reason \"Core library\"
hypha hatch bond set --uri cmn://cmn.dev/b3.abc --with 'mints=[\"https://mint.example.com\"]'
hypha hatch bond remove --relation follows
hypha hatch bond clear")]
Bond {
#[command(subcommand)]
#[serde(flatten)]
command: HatchBondCommands,
},
Tree {
#[command(subcommand)]
#[serde(flatten)]
command: HatchTreeCommands,
},
}
#[derive(Subcommand, Serialize)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum HatchBondCommands {
Set {
#[arg(long)]
uri: String,
#[arg(long)]
relation: Option<substrate::BondRelation>,
#[arg(long)]
id: Option<String>,
#[arg(long)]
reason: Option<String>,
#[arg(long = "with", value_name = "KEY=VALUE")]
with_entries: Vec<String>,
},
Remove {
#[arg(long)]
uri: Option<String>,
#[arg(long)]
relation: Option<substrate::BondRelation>,
},
Clear,
}
#[derive(Subcommand, Serialize)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum HatchTreeCommands {
Set {
#[arg(long)]
algorithm: Option<String>,
#[arg(long, num_args = 1..)]
exclude_names: Option<Vec<String>>,
#[arg(long, num_args = 1..)]
follow_rules: Option<Vec<String>>,
},
Show,
}
#[derive(Subcommand, Serialize)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum MyceliumAction {
#[command(after_long_help = "\
Creates ~/.cmn/mycelium/<domain>/ with key pair and site structure.
Run this once before `hypha release`.
With --hub, creates a taste-only account on a hosted hub (e.g. cmnhub.com):
1. Generates ed25519 key pair
2. Computes subdomain from pubkey (ed-<base32>.hub)
3. Creates taste-only cmn.json with taste endpoint
4. Registers hub as a synapse node
5. Sets [defaults.taste] so `hypha taste` auto-submits
After --hub, register with the hub then taste without extra flags:
curl -X POST https://cmnhub.com/synapse/pulse -H 'Content-Type: application/json' \\
-d @~/.cmn/mycelium/ed-xxx.cmnhub.com/public/.well-known/cmn.json
hypha taste cmn://example.com/b3.HASH --verdict safe
Examples:
hypha mycelium root cmn.dev --name \"CMN\" --synopsis \"Code Mycelial Network\"
hypha mycelium root cmn.dev --endpoints-base https://cmn.dev
hypha mycelium root example.com --site-path /custom/path
hypha mycelium root --hub cmnhub.com")]
Root {
domain: Option<String>,
#[arg(long, conflicts_with = "endpoints_base")]
hub: Option<String>,
#[arg(long)]
site_path: Option<String>,
#[arg(long)]
name: Option<String>,
#[arg(long)]
synopsis: Option<String>,
#[arg(long)]
bio: Option<String>,
#[arg(long)]
endpoints_base: Option<String>,
},
#[command(after_long_help = "\
Examples:
hypha mycelium status
hypha mycelium status cmn.dev")]
Status {
domain: Option<String>,
#[arg(long)]
site_path: Option<String>,
},
#[command(after_long_help = "\
Examples:
hypha mycelium serve
hypha mycelium serve cmn.dev --port 3000")]
Serve {
domain: Option<String>,
#[arg(long)]
site_path: Option<String>,
#[arg(long, default_value = "8080")]
port: u16,
},
#[command(after_long_help = "\
Examples:
hypha mycelium nutrient add cmn.dev --type lightning_address --with address=user@example.com
hypha mycelium nutrient add cmn.dev --type url --with url=https://example.com --with label=Donate
hypha mycelium nutrient remove cmn.dev --type url
hypha mycelium nutrient clear cmn.dev")]
Nutrient {
#[command(subcommand)]
#[serde(flatten)]
command: NutrientCommands,
},
#[command(after_long_help = "\
Examples:
hypha mycelium pulse --synapse synapse.cmn.dev --file ~/.cmn/mycelium/cmn.dev/public/cmn/mycelium/<hash>.json
hypha mycelium pulse --synapse https://synapse.cmn.dev --file ~/.cmn/mycelium/cmn.dev/public/cmn/mycelium/<hash>.json")]
Pulse {
#[arg(long)]
synapse: Option<String>,
#[arg(long)]
synapse_token_secret: Option<String>,
#[arg(long)]
file: String,
},
}
#[derive(Subcommand, Serialize)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum NutrientCommands {
Add {
domain: String,
#[arg(long = "type", value_name = "TYPE")]
method_type: String,
#[arg(long = "with", value_name = "KEY=VALUE")]
with_entries: Vec<String>,
#[arg(long)]
site_path: Option<String>,
},
Remove {
domain: String,
#[arg(long = "type", value_name = "TYPE")]
method_type: String,
#[arg(long)]
site_path: Option<String>,
},
Clear {
domain: String,
#[arg(long)]
site_path: Option<String>,
},
}
#[derive(Subcommand, Serialize)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum SynapseAction {
#[command(after_long_help = "\
Examples:
hypha synapse discover
hypha synapse discover --synapse https://synapse.cmn.dev")]
Discover {
#[arg(long)]
synapse: Option<String>,
#[arg(long)]
synapse_token_secret: Option<String>,
},
#[command(after_long_help = "\
Examples:
hypha synapse list")]
List,
#[command(after_long_help = "\
Examples:
hypha synapse health
hypha synapse health synapse.cmn.dev
hypha synapse health https://synapse.cmn.dev")]
Health {
synapse: Option<String>,
#[arg(long)]
synapse_token_secret: Option<String>,
},
#[command(after_long_help = "\
Examples:
hypha synapse add https://synapse.cmn.dev")]
Add {
url: String,
},
#[command(after_long_help = "\
Examples:
hypha synapse remove synapse.cmn.dev")]
Remove {
domain: String,
},
#[command(after_long_help = "\
Examples:
hypha synapse use synapse.cmn.dev")]
Use {
domain: String,
},
#[command(after_long_help = "\
Examples:
hypha synapse config synapse.cmn.dev --token-secret sk-abc123
hypha synapse config synapse.cmn.dev --token-secret \"\" # clear token")]
Config {
domain: String,
#[arg(long)]
token_secret: Option<String>,
},
}
#[derive(Subcommand, Serialize)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum CacheAction {
#[command(after_long_help = "\
Examples:
hypha cache list
hypha cache list -o yaml")]
List,
#[command(after_long_help = "\
Examples:
hypha cache clean --all")]
Clean {
#[arg(long)]
all: bool,
},
#[command(after_long_help = concat!(
"Examples:\n",
" hypha cache path cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
))]
Path {
uri: String,
},
}
#[derive(Subcommand, Serialize)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum ConfigAction {
#[command(after_long_help = "\
Examples:
hypha config list
hypha config list -o yaml")]
List,
#[command(after_long_help = "\
Dotted keys map to TOML sections:
cache.path Custom cache directory
cache.cmn_ttl_s cmn.json cache TTL in seconds
cache.key_trust_ttl_s Key trust cache TTL in seconds
cache.key_trust_refresh_mode
Key trust refresh mode: expired | always | offline
cache.key_trust_synapse_witness_mode
Key trust fallback when domain is offline: allow | require_domain
cache.clock_skew_tolerance_s
Clock skew tolerance in seconds for key trust TTL (default: 300)
defaults.synapse Default synapse domain
defaults.domain Default domain for publishing (release)
defaults.taste.synapse
Synapse to submit taste reports to (overrides defaults.synapse for taste)
defaults.taste.domain Domain to sign taste reports with (overrides defaults.domain for taste)
Examples:
hypha config set cache.cmn_ttl_s 600
hypha config set cache.key_trust_ttl_s 604800
hypha config set cache.key_trust_refresh_mode offline
hypha config set cache.key_trust_synapse_witness_mode require_domain
hypha config set cache.path /tmp/hypha-cache
hypha config set defaults.synapse synapse.cmn.dev
hypha config set defaults.taste.synapse cmnhub.com
hypha config set defaults.taste.domain ed-xxx.cmnhub.com")]
Set {
key: String,
value: String,
},
}
pub fn parse_or_exit() -> Cli {
let raw: Vec<String> = std::env::args().collect();
if raw.iter().any(|a| a == "--help" || a == "-h") {
let subcommand_path: Vec<&str> = raw[1..]
.iter()
.take_while(|a| !a.starts_with('-'))
.map(|s| s.as_str())
.collect();
let mut stdout = std::io::stdout();
let _ = std::io::Write::write_all(
&mut stdout,
agent_first_data::cli_render_help(&Cli::command(), &subcommand_path).as_bytes(),
);
std::process::exit(0);
}
if raw.iter().any(|a| a == "--help-markdown") {
let subcommand_path: Vec<&str> = raw[1..]
.iter()
.take_while(|a| !a.starts_with('-'))
.map(|s| s.as_str())
.collect();
let mut stdout = std::io::stdout();
let _ = std::io::Write::write_all(
&mut stdout,
agent_first_data::cli_render_help_markdown(&Cli::command(), &subcommand_path)
.as_bytes(),
);
std::process::exit(0);
}
Cli::try_parse().unwrap_or_else(|e| {
if matches!(e.kind(), clap::error::ErrorKind::DisplayVersion) {
let mut stdout = std::io::stdout();
let message = agent_first_data::output_json(&agent_first_data::build_json_ok(
serde_json::json!({ "version": env!("CARGO_PKG_VERSION") }),
None,
));
let _ = std::io::Write::write_all(&mut stdout, message.as_bytes());
let _ = std::io::Write::write_all(&mut stdout, b"\n");
std::process::exit(0);
}
let mut stdout = std::io::stdout();
let message =
agent_first_data::output_json(&agent_first_data::build_cli_error(&e.to_string(), None));
let _ = std::io::Write::write_all(&mut stdout, message.as_bytes());
let _ = std::io::Write::write_all(&mut stdout, b"\n");
std::process::exit(2);
})
}