1use clap::{CommandFactory, Parser, Subcommand};
7use serde::Serialize;
8
9#[derive(Parser)]
10#[command(name = "hypha")]
11#[command(version)]
12#[command(about = "CMN Client - A bio-digital extension for Visitors to release and absorb Spores")]
13#[command(after_long_help = concat!(
14 "All output follows Agent-First Data format:\n",
15 " {\"code\": \"ok\", \"result\": {...}, \"trace\": {...}}\n",
16 "\n",
17 "Quick start (try with cmn.dev):\n",
18 " hypha sense cmn://cmn.dev\n",
19 " hypha sense cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2", "\n",
20 " hypha spawn cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2", "\n",
21 " hypha cache list",
22))]
23pub struct Cli {
24 #[arg(short, long, default_value = "json", global = true)]
26 pub output: String,
27
28 #[arg(long, value_delimiter = ',', global = true)]
30 pub log: Vec<String>,
31
32 #[command(subcommand)]
33 pub command: Commands,
34}
35
36#[derive(Subcommand, Serialize)]
37#[serde(tag = "command", rename_all = "snake_case")]
38pub enum Commands {
39 #[command(after_long_help = concat!(
44 "URI types:\n",
45 " cmn://DOMAIN List all spores on a site\n",
46 " cmn://DOMAIN/HASH View a specific spore\n",
47 "\n",
48 "Examples:\n",
49 " hypha sense cmn://cmn.dev\n",
50 " hypha sense cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2", "\n",
51 " hypha sense cmn://cmn.dev -o yaml",
52 ))]
53 Sense {
54 uri: String,
56 },
57
58 #[command(after_long_help = concat!(
60 "Without --verdict: downloads the spore for local review.\n",
61 "With --verdict: records a verdict (sweet, fresh, safe, rotten, toxic).\n",
62 "\n",
63 "Examples:\n",
64 " hypha taste cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2", "\n",
65 " hypha taste cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
66 " --verdict safe --notes \"Reviewed: clean code\"\n",
67 " hypha taste cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
68 " --verdict safe --domain cmn.dev --synapse https://synapse.cmn.dev",
69 ))]
70 Taste {
71 uri: String,
73 #[arg(long, value_name = "VERDICT")]
75 verdict: Option<substrate::TasteVerdict>,
76 #[arg(long)]
78 notes: Option<String>,
79 #[arg(long)]
81 synapse: Option<String>,
82 #[arg(long)]
84 synapse_token_secret: Option<String>,
85 #[arg(long)]
87 domain: Option<String>,
88 },
89
90 #[command(after_long_help = concat!(
92 "Distribution sources (auto-detected):\n",
93 " archive Download .tar.zst archive (default, fastest)\n",
94 " git Clone from dist.git URL if available\n",
95 "\n",
96 "Examples:\n",
97 " hypha spawn cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2", "\n",
98 " hypha spawn cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
99 " my-project --vcs git\n",
100 " hypha spawn cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
101 " --dist git",
102 ))]
103 Spawn {
104 uri: String,
106 directory: Option<String>,
108 #[arg(long, value_name = "TYPE")]
110 vcs: Option<String>,
111 #[arg(long, value_name = "SOURCE")]
113 dist: Option<String>,
114 #[arg(long)]
116 bond: bool,
117 },
118
119 #[command(after_long_help = "\
121Run inside a previously spawned directory to update it.
122Uses Synapse lineage to discover newer versions from the same publisher.
123
124If local files have been modified (git dirty or tree hash mismatch),
125grow refuses to overwrite them and shows the cache path for manual merge.
126
127Examples:
128 hypha grow
129 hypha grow --synapse synapse.cmn.dev
130 hypha grow --dist git
131 hypha grow --dist archive
132 hypha grow --bond --synapse synapse.cmn.dev")]
133 Grow {
134 #[arg(long, value_name = "SOURCE")]
136 dist: Option<String>,
137 #[arg(long)]
139 synapse: Option<String>,
140 #[arg(long)]
142 synapse_token_secret: Option<String>,
143 #[arg(long)]
145 bond: bool,
146 },
147
148 #[command(after_long_help = concat!(
150 "Absorb downloads spores into .cmn/absorb/ for AI-assisted merge.\n",
151 "Use --discover to auto-discover descendants via Synapse.\n",
152 "\n",
153 "Examples:\n",
154 " hypha absorb cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2", "\n",
155 " hypha absorb --discover --synapse https://synapse.cmn.dev\n",
156 " hypha absorb --discover --synapse https://synapse.cmn.dev --max-depth 5",
157 ))]
158 Absorb {
159 #[arg(required_unless_present = "discover")]
161 uris: Vec<String>,
162 #[arg(long)]
164 discover: bool,
165 #[arg(long)]
167 synapse: Option<String>,
168 #[arg(long)]
170 synapse_token_secret: Option<String>,
171 #[arg(long, default_value = "10")]
173 max_depth: u32,
174 },
175
176 #[command(after_long_help = "\
178Examples:
179 hypha bond
180 hypha bond --status
181 hypha bond --clean")]
182 Bond {
183 #[arg(long)]
185 clean: bool,
186 #[arg(long)]
188 status: bool,
189 },
190
191 #[command(after_long_help = "\
193Replicates spores from another domain to yours. The hash stays the same
194because core + core_signature are preserved. Only capsule_signature changes.
195
196Examples:
197 hypha replicate cmn://other.dev/HASH --domain my.dev
198 hypha replicate --refs --domain my.dev")]
199 Replicate {
200 #[arg(required_unless_present = "refs")]
202 uris: Vec<String>,
203 #[arg(long)]
205 refs: bool,
206 #[arg(long)]
208 domain: String,
209 #[arg(long)]
211 site_path: Option<String>,
212 },
213
214 #[command(after_long_help = "\
216Examples:
217 hypha hatch --id my-tool --name \"My Tool\" --synopsis \"A useful tool\"
218 hypha hatch --intent \"Provide a reusable HTTP client for CMN agents\" --mutations \"Initial release\"
219 hypha hatch --license MIT --domain cmn.dev
220
221Subcommands:
222 hypha hatch bond set/remove/clear Manage bonds in spore.core.json
223 hypha hatch tree set/show Manage tree configuration")]
224 #[command(args_conflicts_with_subcommands = true)]
225 Hatch {
226 #[arg(long)]
228 id: Option<String>,
229 #[arg(long)]
231 version: Option<String>,
232 #[arg(long)]
234 name: Option<String>,
235 #[arg(long)]
237 domain: Option<String>,
238 #[arg(long)]
240 synopsis: Option<String>,
241 #[arg(long)]
243 intent: Vec<String>,
244 #[arg(long)]
246 mutations: Vec<String>,
247 #[arg(long)]
249 license: Option<String>,
250
251 #[command(subcommand)]
252 #[serde(skip)]
253 command: Option<HatchCommands>,
254 },
255
256 #[command(after_long_help = "\
258Requires `hypha mycelium root` first to set up the site.
259
260Examples:
261 hypha release --domain cmn.dev
262 hypha release --domain cmn.dev --source ./my-spore
263 hypha release --domain cmn.dev --dry-run # pre-compute URI without releasing
264 hypha release --domain cmn.dev --archive zstd
265 hypha release --domain cmn.dev --dist-git https://github.com/user/repo --dist-ref v1.0")]
266 Release {
267 #[arg(long)]
269 domain: String,
270 #[arg(long)]
272 source: Option<String>,
273 #[arg(long)]
275 site_path: Option<String>,
276 #[arg(long)]
278 dist_git: Option<String>,
279 #[arg(long)]
281 dist_ref: Option<String>,
282 #[arg(long, value_name = "FORMAT", default_value = "zstd")]
284 archive: String,
285 #[arg(long)]
287 dry_run: bool,
288 },
289
290 #[command(after_long_help = concat!(
295 "Direction:\n",
296 " --direction in Find descendants / forks (default)\n",
297 " --direction out Trace ancestors / spawn chain\n",
298 "\n",
299 "Examples:\n",
300 " hypha lineage cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
301 " --synapse https://synapse.cmn.dev\n",
302 " hypha lineage cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
303 " --direction out --synapse https://synapse.cmn.dev\n",
304 " hypha lineage cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
305 " --synapse https://synapse.cmn.dev --max-depth 5",
306 ))]
307 Lineage {
308 uri: String,
310 #[arg(long)]
312 direction: Option<String>,
313 #[arg(long)]
315 synapse: Option<String>,
316 #[arg(long)]
318 synapse_token_secret: Option<String>,
319 #[arg(long, default_value = "10")]
321 max_depth: u32,
322 },
323
324 #[command(after_long_help = "\
326Examples:
327 hypha search \"protocol spec\" --synapse https://synapse.cmn.dev
328 hypha search \"data format\" --synapse https://synapse.cmn.dev --domain cmn.dev
329 hypha search \"agent tools\" --synapse https://synapse.cmn.dev --license MIT --limit 5
330 hypha search \"http client\" --bonds spawned_from:cmn://cmn.dev/b3.abc123")]
331 Search {
332 query: String,
334 #[arg(long)]
336 synapse: Option<String>,
337 #[arg(long)]
339 synapse_token_secret: Option<String>,
340 #[arg(long)]
342 domain: Option<String>,
343 #[arg(long)]
345 license: Option<String>,
346 #[arg(long)]
348 bonds: Option<String>,
349 #[arg(long, default_value = "20")]
351 limit: u32,
352 },
353
354 Mycelium {
359 #[command(subcommand)]
360 #[serde(flatten)]
361 action: MyceliumAction,
362 },
363
364 Synapse {
366 #[command(subcommand)]
367 #[serde(flatten)]
368 action: SynapseAction,
369 },
370
371 Cache {
373 #[command(subcommand)]
374 #[serde(flatten)]
375 action: CacheAction,
376 },
377
378 Config {
380 #[command(subcommand)]
381 #[serde(flatten)]
382 action: ConfigAction,
383 },
384}
385
386#[derive(Subcommand, Serialize)]
387#[serde(tag = "action", rename_all = "snake_case")]
388pub enum HatchCommands {
389 #[command(after_long_help = "\
391Examples:
392 hypha hatch bond set --uri cmn://cmn.dev/b3.abc --relation follows --id my-lib --reason \"Core library\"
393 hypha hatch bond set --uri cmn://cmn.dev/b3.abc --with 'mints=[\"https://mint.example.com\"]'
394 hypha hatch bond remove --relation follows
395 hypha hatch bond clear")]
396 Bond {
397 #[command(subcommand)]
398 #[serde(flatten)]
399 command: HatchBondCommands,
400 },
401 Tree {
403 #[command(subcommand)]
404 #[serde(flatten)]
405 command: HatchTreeCommands,
406 },
407}
408
409#[derive(Subcommand, Serialize)]
410#[serde(tag = "action", rename_all = "snake_case")]
411pub enum HatchBondCommands {
412 Set {
414 #[arg(long)]
416 uri: String,
417 #[arg(long)]
419 relation: Option<substrate::BondRelation>,
420 #[arg(long)]
422 id: Option<String>,
423 #[arg(long)]
425 reason: Option<String>,
426 #[arg(long = "with", value_name = "KEY=VALUE")]
428 with_entries: Vec<String>,
429 },
430 Remove {
432 #[arg(long)]
434 uri: Option<String>,
435 #[arg(long)]
437 relation: Option<substrate::BondRelation>,
438 },
439 Clear,
441}
442
443#[derive(Subcommand, Serialize)]
444#[serde(tag = "action", rename_all = "snake_case")]
445pub enum HatchTreeCommands {
446 Set {
448 #[arg(long)]
450 algorithm: Option<String>,
451 #[arg(long, num_args = 1..)]
453 exclude_names: Option<Vec<String>>,
454 #[arg(long, num_args = 1..)]
456 follow_rules: Option<Vec<String>>,
457 },
458 Show,
460}
461
462#[derive(Subcommand, Serialize)]
463#[serde(tag = "action", rename_all = "snake_case")]
464pub enum MyceliumAction {
465 #[command(after_long_help = "\
467Creates ~/.cmn/mycelium/<domain>/ with key pair and site structure.
468Run this once before `hypha release`.
469
470With --hub, creates a taste-only account on a hosted hub (e.g. cmnhub.com):
471 1. Generates ed25519 key pair
472 2. Computes subdomain from pubkey (ed-<base32>.hub)
473 3. Creates taste-only cmn.json with taste endpoint
474 4. Registers hub as a synapse node
475 5. Sets [defaults.taste] so `hypha taste` auto-submits
476
477After --hub, register with the hub then taste without extra flags:
478 curl -X POST https://cmnhub.com/synapse/pulse -H 'Content-Type: application/json' \\
479 -d @~/.cmn/mycelium/ed-xxx.cmnhub.com/public/.well-known/cmn.json
480 hypha taste cmn://example.com/b3.HASH --verdict safe
481
482Examples:
483 hypha mycelium root cmn.dev --name \"CMN\" --synopsis \"Code Mycelial Network\"
484 hypha mycelium root cmn.dev --endpoints-base https://cmn.dev
485 hypha mycelium root example.com --site-path /custom/path
486 hypha mycelium root --hub cmnhub.com")]
487 Root {
488 domain: Option<String>,
490 #[arg(long, conflicts_with = "endpoints_base")]
493 hub: Option<String>,
494 #[arg(long)]
496 site_path: Option<String>,
497 #[arg(long)]
499 name: Option<String>,
500 #[arg(long)]
502 synopsis: Option<String>,
503 #[arg(long)]
505 bio: Option<String>,
506 #[arg(long)]
508 endpoints_base: Option<String>,
509 },
510 #[command(after_long_help = "\
512Examples:
513 hypha mycelium status
514 hypha mycelium status cmn.dev")]
515 Status {
516 domain: Option<String>,
518 #[arg(long)]
520 site_path: Option<String>,
521 },
522 #[command(after_long_help = "\
524Examples:
525 hypha mycelium serve
526 hypha mycelium serve cmn.dev --port 3000")]
527 Serve {
528 domain: Option<String>,
530 #[arg(long)]
532 site_path: Option<String>,
533 #[arg(long, default_value = "8080")]
535 port: u16,
536 },
537 #[command(after_long_help = "\
539Examples:
540 hypha mycelium nutrient add cmn.dev --type lightning_address --with address=user@example.com
541 hypha mycelium nutrient add cmn.dev --type url --with url=https://example.com --with label=Donate
542 hypha mycelium nutrient remove cmn.dev --type url
543 hypha mycelium nutrient clear cmn.dev")]
544 Nutrient {
545 #[command(subcommand)]
546 #[serde(flatten)]
547 command: NutrientCommands,
548 },
549 #[command(after_long_help = "\
551Examples:
552 hypha mycelium pulse --synapse synapse.cmn.dev --file ~/.cmn/mycelium/cmn.dev/public/cmn/mycelium/<hash>.json
553 hypha mycelium pulse --synapse https://synapse.cmn.dev --file ~/.cmn/mycelium/cmn.dev/public/cmn/mycelium/<hash>.json")]
554 Pulse {
555 #[arg(long)]
557 synapse: Option<String>,
558 #[arg(long)]
560 synapse_token_secret: Option<String>,
561 #[arg(long)]
563 file: String,
564 },
565}
566
567#[derive(Subcommand, Serialize)]
568#[serde(tag = "action", rename_all = "snake_case")]
569pub enum NutrientCommands {
570 Add {
572 domain: String,
574 #[arg(long = "type", value_name = "TYPE")]
576 method_type: String,
577 #[arg(long = "with", value_name = "KEY=VALUE")]
579 with_entries: Vec<String>,
580 #[arg(long)]
582 site_path: Option<String>,
583 },
584 Remove {
586 domain: String,
588 #[arg(long = "type", value_name = "TYPE")]
590 method_type: String,
591 #[arg(long)]
593 site_path: Option<String>,
594 },
595 Clear {
597 domain: String,
599 #[arg(long)]
601 site_path: Option<String>,
602 },
603}
604
605#[derive(Subcommand, Serialize)]
606#[serde(tag = "action", rename_all = "snake_case")]
607pub enum SynapseAction {
608 #[command(after_long_help = "\
610Examples:
611 hypha synapse discover
612 hypha synapse discover --synapse https://synapse.cmn.dev")]
613 Discover {
614 #[arg(long)]
616 synapse: Option<String>,
617 #[arg(long)]
619 synapse_token_secret: Option<String>,
620 },
621 #[command(after_long_help = "\
623Examples:
624 hypha synapse list")]
625 List,
626 #[command(after_long_help = "\
628Examples:
629 hypha synapse health
630 hypha synapse health synapse.cmn.dev
631 hypha synapse health https://synapse.cmn.dev")]
632 Health {
633 synapse: Option<String>,
635 #[arg(long)]
637 synapse_token_secret: Option<String>,
638 },
639 #[command(after_long_help = "\
641Examples:
642 hypha synapse add https://synapse.cmn.dev")]
643 Add {
644 url: String,
646 },
647 #[command(after_long_help = "\
649Examples:
650 hypha synapse remove synapse.cmn.dev")]
651 Remove {
652 domain: String,
654 },
655 #[command(after_long_help = "\
657Examples:
658 hypha synapse use synapse.cmn.dev")]
659 Use {
660 domain: String,
662 },
663 #[command(after_long_help = "\
665Examples:
666 hypha synapse config synapse.cmn.dev --token-secret sk-abc123
667 hypha synapse config synapse.cmn.dev --token-secret \"\" # clear token")]
668 Config {
669 domain: String,
671 #[arg(long)]
673 token_secret: Option<String>,
674 },
675}
676
677#[derive(Subcommand, Serialize)]
678#[serde(tag = "action", rename_all = "snake_case")]
679pub enum CacheAction {
680 #[command(after_long_help = "\
682Examples:
683 hypha cache list
684 hypha cache list -o yaml")]
685 List,
686 #[command(after_long_help = "\
688Examples:
689 hypha cache clean --all")]
690 Clean {
691 #[arg(long)]
693 all: bool,
694 },
695 #[command(after_long_help = concat!(
697 "Examples:\n",
698 " hypha cache path cmn://cmn.dev/", "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
699 ))]
700 Path {
701 uri: String,
703 },
704}
705
706#[derive(Subcommand, Serialize)]
707#[serde(tag = "action", rename_all = "snake_case")]
708pub enum ConfigAction {
709 #[command(after_long_help = "\
711Examples:
712 hypha config list
713 hypha config list -o yaml")]
714 List,
715 #[command(after_long_help = "\
717Dotted keys map to TOML sections:
718 cache.path Custom cache directory
719 cache.cmn_ttl_s cmn.json cache TTL in seconds
720 cache.key_trust_ttl_s Key trust cache TTL in seconds
721 cache.key_trust_refresh_mode
722 Key trust refresh mode: expired | always | offline
723 cache.key_trust_synapse_witness_mode
724 Key trust fallback when domain is offline: allow | require_domain
725 cache.clock_skew_tolerance_s
726 Clock skew tolerance in seconds for key trust TTL (default: 300)
727 defaults.synapse Default synapse domain
728 defaults.domain Default domain for publishing (release)
729 defaults.taste.synapse
730 Synapse to submit taste reports to (overrides defaults.synapse for taste)
731 defaults.taste.domain Domain to sign taste reports with (overrides defaults.domain for taste)
732
733Examples:
734 hypha config set cache.cmn_ttl_s 600
735 hypha config set cache.key_trust_ttl_s 604800
736 hypha config set cache.key_trust_refresh_mode offline
737 hypha config set cache.key_trust_synapse_witness_mode require_domain
738 hypha config set cache.path /tmp/hypha-cache
739 hypha config set defaults.synapse synapse.cmn.dev
740 hypha config set defaults.taste.synapse cmnhub.com
741 hypha config set defaults.taste.domain ed-xxx.cmnhub.com")]
742 Set {
743 key: String,
745 value: String,
747 },
748}
749
750pub fn parse_or_exit() -> Cli {
751 let raw: Vec<String> = std::env::args().collect();
752
753 if raw.iter().any(|a| a == "--help" || a == "-h") {
755 let subcommand_path: Vec<&str> = raw[1..]
756 .iter()
757 .take_while(|a| !a.starts_with('-'))
758 .map(|s| s.as_str())
759 .collect();
760 let mut stdout = std::io::stdout();
761 let _ = std::io::Write::write_all(
762 &mut stdout,
763 agent_first_data::cli_render_help(&Cli::command(), &subcommand_path).as_bytes(),
764 );
765 std::process::exit(0);
766 }
767 if raw.iter().any(|a| a == "--help-markdown") {
769 let subcommand_path: Vec<&str> = raw[1..]
770 .iter()
771 .take_while(|a| !a.starts_with('-'))
772 .map(|s| s.as_str())
773 .collect();
774 let mut stdout = std::io::stdout();
775 let _ = std::io::Write::write_all(
776 &mut stdout,
777 agent_first_data::cli_render_help_markdown(&Cli::command(), &subcommand_path)
778 .as_bytes(),
779 );
780 std::process::exit(0);
781 }
782
783 Cli::try_parse().unwrap_or_else(|e| {
784 if matches!(e.kind(), clap::error::ErrorKind::DisplayVersion) {
785 let mut stdout = std::io::stdout();
786 let message = agent_first_data::output_json(&agent_first_data::build_json_ok(
787 serde_json::json!({ "version": env!("CARGO_PKG_VERSION") }),
788 None,
789 ));
790 let _ = std::io::Write::write_all(&mut stdout, message.as_bytes());
791 let _ = std::io::Write::write_all(&mut stdout, b"\n");
792 std::process::exit(0);
793 }
794
795 let mut stdout = std::io::stdout();
796 let message =
797 agent_first_data::output_json(&agent_first_data::build_cli_error(&e.to_string(), None));
798 let _ = std::io::Write::write_all(&mut stdout, message.as_bytes());
799 let _ = std::io::Write::write_all(&mut stdout, b"\n");
800 std::process::exit(2);
801 })
802}