1use anyhow::Result;
4use clap::Parser;
5use std::path::PathBuf;
6use tracing::info;
7
8pub mod auth;
9pub mod commands;
10pub mod config;
11pub mod credential_pool;
12pub mod cron;
13pub mod error;
14pub mod gateway;
15pub mod mcp;
16pub mod pairings;
17pub mod plugins;
18pub mod profiles;
19pub mod skills;
20pub mod skills_store;
21pub mod tools;
22pub mod webhooks;
23
24pub use config::Config;
25pub use error::CliError;
26
27#[derive(Parser, Debug)]
30#[command(name = "hermes", about = "Hermes Agent CLI", version, author)]
31pub struct Cli {
32 #[arg(short, long, global = true)]
33 verbose: bool,
34 #[arg(short, long, global = true)]
35 debug: bool,
36 #[arg(short = 'p', long, global = true, value_name = "NAME")]
37 profile: Option<String>,
38 #[arg(long, global = true, value_name = "PATH")]
39 directory: Option<PathBuf>,
40 #[arg(long, global = true, value_name = "SESSION_ID")]
42 resume: Option<String>,
43 #[arg(short = 'c', long = "continue", global = true, value_name = "SESSION_NAME")]
45 continue_last: Option<Option<String>>,
46 #[command(subcommand)]
47 pub command: Option<Commands>,
48}
49
50#[derive(clap::Subcommand, Debug, Clone)]
53pub enum Commands {
54 Chat {
56 model: Option<String>,
57 #[arg(short, long)]
59 query: Option<String>,
60 #[arg(long)]
62 image: Option<String>,
63 #[arg(short, long)]
65 system: Option<String>,
66 #[arg(short, long)]
68 toolsets: Option<String>,
69 #[arg(long)]
71 skills: Option<Vec<String>>,
72 #[arg(long)]
74 provider: Option<String>,
75 #[arg(long)]
77 chat_verbose: bool,
78 #[arg(short = 'Q', long)]
80 quiet: bool,
81 #[arg(short, long)]
83 resume: Option<String>,
84 #[arg(short = 'n', long = "continue")]
86 continue_last: Option<Option<String>>,
87 #[arg(short, long)]
89 worktree: bool,
90 #[arg(long)]
92 checkpoints: bool,
93 #[arg(long)]
95 max_turns: Option<u32>,
96 #[arg(long)]
98 yolo: bool,
99 #[arg(long)]
101 pass_session_id: bool,
102 #[arg(long)]
104 source: Option<String>,
105 },
106
107 #[command(subcommand)]
109 Auth(AuthCommand),
110
111 Model {
113 #[arg(short = 'C', long)]
114 current: bool,
115 #[arg(long)]
116 global: bool,
117 model: Option<String>,
118 #[arg(long)]
119 portal_url: Option<String>,
120 #[arg(long)]
121 inference_url: Option<String>,
122 #[arg(long)]
123 client_id: Option<String>,
124 #[arg(long)]
125 scope: Option<String>,
126 #[arg(long)]
127 no_browser: bool,
128 #[arg(long, default_value = "15.0")]
129 timeout: f64,
130 #[arg(long)]
131 ca_bundle: Option<String>,
132 #[arg(long)]
133 insecure: bool,
134 },
135
136 #[command(subcommand)]
138 Tools(ToolsCommand),
139
140 #[command(subcommand)]
142 Skills(SkillsCommand),
143
144 #[command(subcommand)]
146 Gateway(GatewayCommand),
147
148 #[command(subcommand)]
150 Cron(CronCommand),
151
152 #[command(subcommand)]
154 Config(ConfigCommand),
155
156 Setup {
158 section: Option<String>,
160 #[arg(long)]
161 skip_auth: bool,
162 #[arg(long)]
163 skip_model: bool,
164 #[arg(long)]
165 non_interactive: bool,
166 #[arg(long)]
167 reset: bool,
168 },
169
170 Doctor {
172 #[arg(short, long)]
173 all: bool,
174 check: Option<String>,
176 #[arg(long)]
177 fix: bool,
178 },
179
180 Status {
182 #[arg(long)]
183 all: bool,
184 #[arg(long)]
185 deep: bool,
186 },
187
188 #[command(subcommand)]
190 Sessions(SessionsCommand),
191
192 Logs {
194 log_name: Option<String>,
196 #[arg(long, default_value = "50")]
198 lines: u32,
199 #[arg(short, long)]
201 follow: bool,
202 #[arg(long)]
204 level: Option<String>,
205 #[arg(long)]
207 session: Option<String>,
208 #[arg(long)]
210 since: Option<String>,
211 #[arg(long)]
213 component: Option<String>,
214 },
215
216 #[command(subcommand)]
218 Profile(ProfileCommand),
219
220 #[command(subcommand)]
222 Mcp(McpCommand),
223
224 #[command(subcommand)]
226 Memory(MemoryCommand),
227
228 #[command(subcommand)]
230 Webhook(WebhookCommand),
231
232 #[command(subcommand)]
234 Pairing(PairingCommand),
235
236 #[command(subcommand)]
238 Plugins(PluginsCommand),
239
240 Backup {
242 #[arg(short, long)]
244 output: Option<String>,
245 #[arg(short, long)]
247 quick: bool,
248 #[arg(short, long)]
250 label: Option<String>,
251 },
252
253 Import {
255 zipfile: String,
257 #[arg(short, long)]
259 force: bool,
260 },
261
262 #[command(subcommand)]
264 Debug(DebugCommand),
265
266 Dump {
268 #[arg(long)]
270 show_keys: bool,
271 },
272
273 Completion {
275 shell: Option<String>,
277 },
278
279 Insights {
281 #[arg(long, default_value = "30")]
283 days: u32,
284 #[arg(long)]
286 source: Option<String>,
287 },
288
289 Login {
291 #[arg(long)]
292 provider: Option<String>,
293 #[arg(long)]
294 portal_url: Option<String>,
295 #[arg(long)]
296 inference_url: Option<String>,
297 #[arg(long)]
298 client_id: Option<String>,
299 #[arg(long)]
300 scope: Option<String>,
301 #[arg(long)]
302 no_browser: bool,
303 #[arg(long, default_value = "15.0")]
304 timeout: f64,
305 #[arg(long)]
306 ca_bundle: Option<String>,
307 #[arg(long)]
308 insecure: bool,
309 },
310
311 Logout {
313 #[arg(long)]
314 provider: Option<String>,
315 },
316
317 Whatsapp,
319
320 Acp,
322
323 Dashboard {
325 #[arg(long, default_value = "9119")]
326 port: u16,
327 #[arg(long, default_value = "127.0.0.1")]
328 host: String,
329 #[arg(long)]
330 no_open: bool,
331 },
332
333 #[command(subcommand)]
335 Claw(ClawCommand),
336
337 Models {
339 #[arg(long)]
341 provider: Option<String>,
342 #[arg(long)]
344 tools: bool,
345 #[arg(long)]
347 pricing: bool,
348 },
349
350 Version,
352
353 Update {
355 #[arg(long)]
356 gateway: bool,
357 },
358
359 Uninstall {
361 #[arg(long)]
362 full: bool,
363 #[arg(short, long)]
364 yes: bool,
365 },
366}
367
368#[derive(clap::Subcommand, Debug, Clone)]
371#[allow(clippy::large_enum_variant)]
372pub enum AuthCommand {
373 Add {
374 provider: String,
375 #[arg(long)]
377 api_key: Option<String>,
378 #[arg(long = "type", value_name = "AUTH_TYPE")]
380 auth_type: Option<String>,
381 #[arg(long)]
383 label: Option<String>,
384 #[arg(long)]
386 base_url: Option<String>,
387 #[arg(long)]
389 portal_url: Option<String>,
390 #[arg(long)]
392 inference_url: Option<String>,
393 #[arg(long)]
395 client_id: Option<String>,
396 #[arg(long)]
398 scope: Option<String>,
399 #[arg(long)]
401 no_browser: bool,
402 #[arg(long)]
404 timeout: Option<f64>,
405 #[arg(long)]
407 insecure: bool,
408 #[arg(long)]
410 ca_bundle: Option<String>,
411 },
412 List {
413 provider: Option<String>,
415 },
416 Remove {
417 provider: String,
418 target: Option<String>,
420 },
421 Reset {
422 provider: Option<String>,
424 },
425}
426
427#[derive(clap::Subcommand, Debug, Clone)]
428pub enum ToolsCommand {
429 List {
430 #[arg(short, long)]
431 all: bool,
432 #[arg(long, default_value = "cli")]
434 platform: String,
435 },
436 Disable {
437 names: Vec<String>,
439 #[arg(long, default_value = "cli")]
440 platform: String,
441 },
442 Enable {
443 names: Vec<String>,
445 #[arg(long, default_value = "cli")]
446 platform: String,
447 },
448}
449
450#[derive(clap::Subcommand, Debug, Clone)]
451pub enum SkillsCommand {
452 Search {
453 query: Option<String>,
454 #[arg(long, default_value = "all")]
455 source: String,
456 #[arg(long, default_value = "10")]
457 limit: u32,
458 },
459 Browse {
460 #[arg(long, default_value = "1")]
461 page: u32,
462 #[arg(long, default_value = "20")]
463 size: u32,
464 #[arg(long, default_value = "all")]
465 source: String,
466 },
467 Inspect {
468 name: String,
469 },
470 Install {
471 identifier: String,
472 #[arg(long, default_value = "")]
473 category: String,
474 #[arg(long)]
475 force: bool,
476 #[arg(short, long)]
477 yes: bool,
478 },
479 List {
480 #[arg(long, default_value = "all")]
481 source: String,
482 },
483 Check {
484 name: Option<String>,
486 },
487 Update {
488 name: Option<String>,
490 },
491 Audit {
492 name: Option<String>,
494 },
495 Uninstall {
496 name: String,
497 },
498 Publish {
499 skill_path: String,
500 #[arg(long, default_value = "github")]
501 to: String,
502 #[arg(long, default_value = "")]
503 repo: String,
504 },
505 #[command(subcommand)]
506 Snapshot(SkillsSnapshotCommand),
507 #[command(subcommand)]
508 Tap(SkillsTapCommand),
509 Config,
510}
511
512#[derive(clap::Subcommand, Debug, Clone)]
513pub enum SkillsSnapshotCommand {
514 Export {
515 output: String,
517 },
518 Import {
519 input: String,
521 #[arg(long)]
522 force: bool,
523 },
524}
525
526#[derive(clap::Subcommand, Debug, Clone)]
527pub enum SkillsTapCommand {
528 List,
529 Add {
530 repo: String,
532 },
533 Remove {
534 name: String,
535 },
536}
537
538#[derive(clap::Subcommand, Debug, Clone)]
539pub enum GatewayCommand {
540 Run {
541 #[arg(short = 'P', long)]
542 platform: Option<String>,
543 #[arg(long)]
545 verbose: bool,
546 #[arg(short, long)]
548 quiet: bool,
549 #[arg(long)]
551 replace: bool,
552 },
553 Start {
554 #[arg(long)]
555 system: bool,
556 },
557 Stop {
558 #[arg(long)]
559 system: bool,
560 #[arg(long)]
561 all: bool,
562 },
563 Restart {
564 #[arg(long)]
565 system: bool,
566 },
567 Status {
568 #[arg(long)]
569 deep: bool,
570 #[arg(long)]
571 system: bool,
572 },
573 Setup {
574 platform: Option<String>,
575 },
576 Install {
577 #[arg(long)]
578 force: bool,
579 #[arg(long)]
580 system: bool,
581 #[arg(long)]
582 run_as_user: Option<String>,
583 },
584 Uninstall {
585 #[arg(long)]
586 system: bool,
587 },
588}
589
590#[derive(clap::Subcommand, Debug, Clone)]
591pub enum CronCommand {
592 List {
593 #[arg(long)]
594 all: bool,
595 },
596 Add {
597 schedule: String,
598 command: Option<String>,
600 #[arg(long)]
601 name: Option<String>,
602 #[arg(long)]
603 deliver: Option<String>,
604 #[arg(long)]
605 repeat: Option<u32>,
606 #[arg(long)]
607 skill: Option<Vec<String>>,
608 #[arg(long)]
609 script: Option<String>,
610 },
611 Edit {
612 job_id: String,
613 #[arg(long)]
614 schedule: Option<String>,
615 #[arg(long)]
616 prompt: Option<String>,
617 #[arg(long)]
618 name: Option<String>,
619 #[arg(long)]
620 deliver: Option<String>,
621 #[arg(long)]
622 repeat: Option<u32>,
623 #[arg(long)]
624 skill: Option<Vec<String>>,
625 #[arg(long)]
626 add_skill: Option<Vec<String>>,
627 #[arg(long)]
628 remove_skill: Option<Vec<String>>,
629 #[arg(long)]
630 clear_skills: bool,
631 #[arg(long)]
632 script: Option<String>,
633 },
634 Remove {
635 id: String,
636 },
637 Pause {
638 id: String,
639 },
640 Resume {
641 id: String,
642 },
643 Run {
644 id: String,
645 },
646 Status,
647 Tick,
648}
649
650#[derive(clap::Subcommand, Debug, Clone)]
651pub enum ConfigCommand {
652 Show,
653 Edit,
654 Get { key: String },
655 Set { key: String, value: String },
656 Reset,
657 Path,
658 EnvPath,
659 Check,
660 Migrate,
661}
662
663#[derive(clap::Subcommand, Debug, Clone)]
664pub enum SessionsCommand {
665 List {
666 #[arg(long)]
667 source: Option<String>,
668 #[arg(long, default_value = "20")]
669 limit: u32,
670 },
671 Export {
672 output: String,
673 #[arg(long)]
674 source: Option<String>,
675 #[arg(long)]
676 session_id: Option<String>,
677 },
678 Delete {
679 session_id: String,
680 #[arg(short, long)]
681 yes: bool,
682 },
683 Prune {
684 #[arg(long, default_value = "90")]
685 older_than: u32,
686 #[arg(long)]
687 source: Option<String>,
688 #[arg(short, long)]
689 yes: bool,
690 },
691 Stats,
692 Rename {
693 session_id: String,
694 title: Vec<String>,
695 },
696 Browse {
697 #[arg(long)]
698 source: Option<String>,
699 #[arg(long, default_value = "50")]
700 limit: u32,
701 },
702}
703
704#[derive(clap::Subcommand, Debug, Clone)]
705pub enum ProfileCommand {
706 List,
707 Use {
708 profile_name: String,
709 },
710 Create {
711 profile_name: String,
712 #[arg(long)]
713 clone: bool,
714 #[arg(long)]
715 clone_all: bool,
716 #[arg(long)]
717 clone_from: Option<String>,
718 #[arg(long)]
719 no_alias: bool,
720 },
721 Delete {
722 profile_name: String,
723 #[arg(short, long)]
724 yes: bool,
725 },
726 Show {
727 profile_name: String,
728 },
729 Alias {
730 profile_name: String,
731 #[arg(long)]
732 remove: bool,
733 #[arg(long)]
734 alias_name: Option<String>,
735 },
736 Rename {
737 old_name: String,
738 new_name: String,
739 },
740 Export {
741 profile_name: String,
742 #[arg(short, long)]
743 output: Option<String>,
744 },
745 Import {
746 archive: String,
747 #[arg(long)]
748 import_name: Option<String>,
749 },
750}
751
752#[derive(clap::Subcommand, Debug, Clone)]
753pub enum McpCommand {
754 Serve {
755 #[arg(short, long)]
756 verbose: bool,
757 },
758 Add {
759 name: String,
760 #[arg(long)]
761 url: Option<String>,
762 #[arg(long)]
763 command: Option<String>,
764 #[arg(long)]
765 args: Option<Vec<String>>,
766 #[arg(long)]
767 auth: Option<String>,
768 #[arg(long)]
769 preset: Option<String>,
770 #[arg(long)]
771 env: Option<Vec<String>>,
772 },
773 Remove {
774 name: String,
775 },
776 List,
777 Test {
778 name: String,
779 },
780 Configure {
781 name: String,
782 },
783}
784
785#[derive(clap::Subcommand, Debug, Clone)]
786pub enum MemoryCommand {
787 Setup,
788 Status,
789 Off,
790}
791
792#[derive(clap::Subcommand, Debug, Clone)]
793pub enum WebhookCommand {
794 Subscribe {
795 name: String,
797 #[arg(long, default_value = "")]
798 prompt: String,
799 #[arg(long, default_value = "")]
800 events: String,
801 #[arg(long, default_value = "")]
802 description: String,
803 #[arg(long, default_value = "")]
804 skills: String,
805 #[arg(long, default_value = "log")]
806 deliver: String,
807 #[arg(long, default_value = "")]
808 deliver_chat_id: String,
809 #[arg(long, default_value = "")]
810 secret: String,
811 },
812 List,
813 Remove {
814 name: String,
815 },
816 Test {
817 name: String,
818 #[arg(long, default_value = "")]
819 payload: String,
820 },
821}
822
823#[derive(clap::Subcommand, Debug, Clone)]
824pub enum PairingCommand {
825 List,
826 Approve { platform: String, code: String },
827 Revoke { platform: String, user_id: String },
828 ClearPending,
829}
830
831#[derive(clap::Subcommand, Debug, Clone)]
832pub enum PluginsCommand {
833 Install {
834 identifier: String,
835 #[arg(short, long)]
836 force: bool,
837 },
838 Update {
839 name: String,
840 },
841 Remove {
842 name: String,
843 },
844 List,
845 Enable {
846 name: String,
847 },
848 Disable {
849 name: String,
850 },
851}
852
853#[derive(clap::Subcommand, Debug, Clone)]
854pub enum DebugCommand {
855 Share {
856 #[arg(long, default_value = "200")]
857 lines: u32,
858 #[arg(long, default_value = "7")]
859 expire: u32,
860 #[arg(long)]
861 local: bool,
862 },
863}
864
865#[derive(clap::Subcommand, Debug, Clone)]
866pub enum ClawCommand {
867 Migrate {
868 #[arg(long)]
869 source: Option<String>,
870 #[arg(long)]
871 dry_run: bool,
872 #[arg(long, default_value = "full")]
873 preset: String,
874 #[arg(long)]
875 overwrite: bool,
876 #[arg(long)]
877 migrate_secrets: bool,
878 #[arg(long)]
879 workspace_target: Option<String>,
880 #[arg(long, default_value = "skip")]
881 skill_conflict: String,
882 #[arg(short, long)]
883 yes: bool,
884 },
885 Cleanup {
886 #[arg(long)]
887 source: Option<String>,
888 #[arg(long)]
889 dry_run: bool,
890 #[arg(short, long)]
891 yes: bool,
892 },
893}
894
895fn ensure_disk_space(threshold_gb: f64) -> f64 {
900 #[cfg(target_os = "windows")]
901 {
902 let output = std::process::Command::new("powershell")
904 .args(["-NoProfile", "-Command", "(Get-PSDrive C).Free / 1GB"])
905 .output();
906
907 let free_gb = match output {
908 Ok(o) if o.status.success() => {
909 let stdout = String::from_utf8_lossy(&o.stdout);
910 stdout.trim().parse::<f64>().unwrap_or(100.0)
911 }
912 _ => 100.0, };
914
915 if free_gb < threshold_gb {
916 info!(
917 "C drive low: {:.2}GB free (threshold: {:.2}GB), auto-cleaning...",
918 free_gb, threshold_gb
919 );
920
921 let target_dir = std::env::current_dir().unwrap_or_default().join("target");
923 let _ = std::fs::remove_dir_all(&target_dir);
924
925 let home = std::env::var("USERPROFILE").unwrap_or("C:\\Users\\Default".to_string());
927 let dirs_to_clean = [
928 format!("{}\\.cargo\\registry\\cache", home),
929 format!("{}\\.cargo\\registry\\src", home),
930 format!("{}\\.cache", home),
931 format!("{}\\AppData\\Local\\npm-cache", home),
932 ];
933 for dir in &dirs_to_clean {
934 let _ = std::fs::remove_dir_all(dir);
935 }
936
937 let output2 = std::process::Command::new("powershell")
939 .args(["-NoProfile", "-Command", "(Get-PSDrive C).Free / 1GB"])
940 .output();
941
942 let free_gb_after = match output2 {
943 Ok(o) if o.status.success() => {
944 let stdout = String::from_utf8_lossy(&o.stdout);
945 stdout.trim().parse::<f64>().unwrap_or(free_gb)
946 }
947 _ => free_gb,
948 };
949
950 info!("After cleanup: {:.2}GB free", free_gb_after);
951
952 if free_gb_after < threshold_gb / 2.0 {
953 eprintln!(
954 "⚠ WARNING: C drive critically low ({:.2}GB free). Consider manual cleanup.",
955 free_gb_after
956 );
957 }
958
959 free_gb_after
960 } else {
961 free_gb
962 }
963 }
964
965 #[cfg(not(target_os = "windows"))]
966 {
967 threshold_gb + 1.0 }
969}
970
971pub async fn run() -> Result<()> {
972 let cli = Cli::parse();
973 init_logging(cli.verbose, cli.debug);
974 info!("hermes-cli starting...");
975
976 ensure_disk_space(2.0);
978
979 let _config = Config::load()?;
980
981 let command = cli.command.unwrap_or(Commands::Chat {
983 model: None,
984 query: None,
985 image: None,
986 system: None,
987 toolsets: None,
988 skills: None,
989 provider: None,
990 chat_verbose: false,
991 quiet: false,
992 resume: cli.resume,
993 continue_last: cli.continue_last,
994 worktree: false,
995 checkpoints: false,
996 max_turns: None,
997 yolo: false,
998 pass_session_id: false,
999 source: None,
1000 });
1001
1002 match &command {
1003 Commands::Chat {
1004 model,
1005 query,
1006 system,
1007 provider,
1008 max_turns,
1009 yolo,
1010 quiet,
1011 chat_verbose,
1012 ..
1013 } => {
1014 handle_chat(
1015 model.clone(),
1016 query.clone(),
1017 system.clone(),
1018 provider.clone(),
1019 *max_turns,
1020 *yolo,
1021 *quiet,
1022 *chat_verbose,
1023 )
1024 .await?;
1025 }
1026 Commands::Auth(ref cmd) => commands::handle_auth(cmd.clone()).await?,
1027 Commands::Model {
1028 current,
1029 global,
1030 model,
1031 portal_url: _,
1032 inference_url: _,
1033 client_id: _,
1034 scope: _,
1035 no_browser: _,
1036 timeout: _,
1037 ca_bundle: _,
1038 insecure: _,
1039 } => commands::handle_model(*current, *global, model.as_deref())?,
1040 Commands::Tools(ref cmd) => commands::handle_tools(cmd.clone())?,
1041 Commands::Skills(ref cmd) => commands::handle_skills(cmd.clone())?,
1042 Commands::Gateway(ref cmd) => commands::handle_gateway(cmd.clone()).await?,
1043 Commands::Cron(ref cmd) => commands::handle_cron(cmd.clone()).await?,
1044 Commands::Config(ref cmd) => commands::handle_config(cmd.clone())?,
1045 Commands::Setup { section: _, skip_auth, skip_model, non_interactive: _, reset: _ } => {
1046 commands::handle_setup(*skip_auth, *skip_model)?
1047 }
1048 Commands::Doctor { all, check, fix: _ } => commands::handle_doctor(*all, check.as_deref())?,
1049 Commands::Status { all: _, deep: _ } => commands::handle_status()?,
1050 Commands::Version => {
1051 println!("hermes {}", env!("CARGO_PKG_VERSION"));
1052 }
1053 Commands::Update { gateway: _ } => commands::handle_update()?,
1054 Commands::Uninstall { full: _, yes: _ } => commands::handle_uninstall()?,
1055 Commands::Sessions(cmd) => commands::handle_sessions(cmd.clone()),
1057 Commands::Logs { log_name, lines, follow, level, session, since, component } => {
1058 commands::handle_logs(
1059 log_name.as_deref(),
1060 *lines,
1061 *follow,
1062 level.as_deref(),
1063 session.as_deref(),
1064 since.as_deref(),
1065 component.as_deref(),
1066 )?
1067 }
1068 Commands::Profile(cmd) => commands::handle_profile(cmd.clone()),
1069 Commands::Mcp(cmd) => commands::handle_mcp(cmd.clone()),
1070 Commands::Memory(cmd) => commands::handle_memory(cmd.clone())?,
1071 Commands::Webhook(cmd) => commands::handle_webhook(cmd.clone()),
1072 Commands::Pairing(cmd) => commands::handle_pairing(cmd.clone()),
1073 Commands::Plugins(cmd) => commands::handle_plugins(cmd.clone()),
1074 Commands::Backup { output, quick, label } => {
1075 commands::handle_backup(output.clone(), *quick, label.clone())?
1076 }
1077 Commands::Import { zipfile, force } => commands::handle_import(zipfile.clone(), *force)?,
1078 Commands::Debug(cmd) => commands::handle_debug(cmd.clone()),
1079 Commands::Dump { show_keys } => commands::handle_dump(*show_keys)?,
1080 Commands::Completion { shell } => commands::handle_completion(shell.as_deref()),
1081 Commands::Insights { days, source } => commands::handle_insights(*days, source.as_deref())?,
1082 Commands::Login {
1083 provider,
1084 portal_url,
1085 inference_url,
1086 client_id,
1087 scope,
1088 no_browser,
1089 timeout,
1090 ca_bundle,
1091 insecure,
1092 } => commands::handle_login(
1093 provider.as_deref(),
1094 portal_url.as_deref(),
1095 inference_url.as_deref(),
1096 client_id.as_deref(),
1097 scope.as_deref(),
1098 *no_browser,
1099 *timeout,
1100 ca_bundle.as_deref(),
1101 *insecure,
1102 )?,
1103 Commands::Logout { provider } => commands::handle_logout(provider.as_deref())?,
1104 Commands::Whatsapp => commands::handle_whatsapp()?,
1105 Commands::Acp => commands::handle_acp()?,
1106 Commands::Dashboard { port, host, no_open } => {
1107 commands::handle_dashboard(*port, host.to_string(), *no_open)?
1108 }
1109 Commands::Claw(cmd) => commands::handle_claw(cmd.clone()),
1110 Commands::Models { provider, tools, pricing } => {
1111 commands::handle_models(provider.as_deref(), *tools, *pricing)?
1112 }
1113 }
1114 Ok(())
1115}
1116
1117fn init_logging(verbose: bool, debug: bool) {
1118 use tracing_subscriber::EnvFilter;
1119 let level = if debug {
1120 tracing::Level::DEBUG
1121 } else if verbose {
1122 tracing::Level::INFO
1123 } else {
1124 tracing::Level::WARN
1125 };
1126 tracing_subscriber::fmt()
1127 .with_env_filter(EnvFilter::from_default_env().add_directive(level.into()))
1128 .with_target(false)
1129 .init();
1130}
1131
1132fn handle_skill_command(
1134 args: &str,
1135 repl: &mut hermes_agent_runtime::ChatRepl,
1136) -> anyhow::Result<Option<String>> {
1137 let store = crate::skills_store::SkillStore::new()?;
1138 let agent = repl.agent_mut();
1139
1140 let parts: Vec<&str> = args.splitn(3, ' ').collect();
1141 let cmd = parts.first().copied().unwrap_or("");
1142
1143 if args.is_empty() || cmd == "list" {
1144 let skills = store.list_skills()?;
1145 if skills.is_empty() {
1146 return Ok(Some("No skills available.".to_string()));
1147 }
1148 let mut out = String::from("Available skills:\n");
1149 for s in &skills {
1150 let cat = s.category.as_deref().unwrap_or("");
1151 let cat_tag = if cat.is_empty() { String::new() } else { format!(" [{}]", cat) };
1152 out.push_str(&format!(" {}{} — {}\n", s.name, cat_tag, s.description));
1153 }
1154 Ok(Some(out))
1155 } else if cmd == "help" {
1156 Ok(Some("Usage:\n /skill list — list available skills\n /skill <name> — load skill into system prompt\n /skill install <name> <content> — install a new skill\n /skill uninstall <name> — remove a skill\n /skill off — clear skill (reset system prompt)".to_string()))
1157 } else if cmd == "off" {
1158 agent.set_system_prompt(String::new());
1159 Ok(Some("Skill unloaded. System prompt cleared.".to_string()))
1160 } else if cmd == "install" {
1161 let name = parts
1162 .get(1)
1163 .ok_or_else(|| anyhow::anyhow!("usage: /skill install <name> <content>"))?;
1164 let content = parts
1165 .get(2)
1166 .ok_or_else(|| anyhow::anyhow!("usage: /skill install <name> <content>"))?;
1167 store.install_skill(name, content)?;
1168 Ok(Some(format!("Skill '{}' installed.", name)))
1169 } else if cmd == "uninstall" {
1170 let name = parts.get(1).ok_or_else(|| anyhow::anyhow!("usage: /skill uninstall <name>"))?;
1171 let removed = store.uninstall_skill(name)?;
1172 if removed {
1173 Ok(Some(format!("Skill '{}' uninstalled.", name)))
1174 } else {
1175 Ok(Some(format!("Skill '{}' not found.", name)))
1176 }
1177 } else {
1178 let skill = store.load_skill(cmd)?;
1179 agent.set_system_prompt(skill.prompt.clone());
1180 Ok(Some(format!(
1181 "Skill '{}' loaded. System prompt set ({} chars).",
1182 skill.name,
1183 skill.prompt.len()
1184 )))
1185 }
1186}
1187
1188#[allow(clippy::too_many_arguments)]
1190async fn handle_chat(
1191 model: Option<String>,
1192 query: Option<String>,
1193 system: Option<String>,
1194 provider: Option<String>,
1195 max_turns: Option<u32>,
1196 yolo: bool,
1197 quiet: bool,
1198 _verbose: bool,
1199) -> anyhow::Result<()> {
1200 use hermes_agent_runtime::provider::create_provider;
1201 use hermes_agent_runtime::tool::{
1202 browser::BrowserTool,
1203 file::{FileReadTool, FileSearchTool, FileWriteTool},
1204 mcp::McpTool,
1205 terminal::TerminalTool,
1206 web::WebSearchTool,
1207 ToolRegistry,
1208 };
1209 use hermes_agent_runtime::{Agent, AgentConfig, ChatRepl};
1210 use hermes_session_db::SessionStore;
1211
1212 let user_config = crate::config::Config::load().unwrap_or_default();
1214
1215 let provider_str = provider
1217 .as_deref()
1218 .or_else(|| {
1219 if user_config.model.provider.is_empty() {
1220 None
1221 } else {
1222 Some(&user_config.model.provider)
1223 }
1224 })
1225 .unwrap_or("openai");
1226 let provider_type =
1227 provider_str.parse::<hermes_common::Provider>().unwrap_or(hermes_common::Provider::OpenAI);
1228
1229 let auth_store = crate::auth::AuthStore::load().unwrap_or_default();
1231 let cred_pool = crate::credential_pool::CredentialPool::from_auth_store(&auth_store);
1232 let api_key = cred_pool
1233 .get(provider_str)
1234 .map(|c| c.api_key)
1235 .or_else(|| std::env::var(format!("{}_API_KEY", provider_str.to_uppercase())).ok())
1236 .or_else(|| std::env::var("OPENAI_API_KEY").ok());
1237
1238 let api_key = match api_key {
1239 Some(key) if !key.is_empty() => key,
1240 _ => {
1241 anyhow::bail!(
1242 "No API key configured for '{}'. Run: hermes auth add {} --api-key <KEY>",
1243 provider_str,
1244 provider_str
1245 );
1246 }
1247 };
1248
1249 let base_url_owned =
1251 auth_store.get(provider_str).and_then(|c| c.base_url.clone()).or_else(|| {
1252 if user_config.model.base_url.is_empty() {
1253 None
1254 } else {
1255 Some(user_config.model.base_url.clone())
1256 }
1257 });
1258 let base_url = base_url_owned.as_deref();
1259
1260 let model = model.unwrap_or_else(|| {
1262 if !user_config.model.default.is_empty() {
1263 user_config.model.default.clone()
1264 } else {
1265 create_provider(&provider_type, &api_key, base_url).default_model().to_string()
1266 }
1267 });
1268
1269 let provider_box = create_provider(&provider_type, &api_key, base_url);
1270
1271 let mut registry = ToolRegistry::new();
1273 registry.register(Box::new(TerminalTool::new()));
1274 registry.register(Box::new(FileReadTool));
1275 registry.register(Box::new(FileWriteTool));
1276 registry.register(Box::new(FileSearchTool));
1277 registry.register(Box::new(WebSearchTool::new()));
1278 registry.register(Box::new(McpTool));
1279 registry.register(Box::new(BrowserTool));
1280
1281 let home = crate::config::Config::hermes_home();
1283 let db_path = home.join("sessions.db");
1284 let session_store = SessionStore::new(&db_path)
1285 .map_err(|e| anyhow::anyhow!("Failed to open session DB: {}", e))?;
1286
1287 let agent_config = AgentConfig {
1289 max_turns: max_turns.unwrap_or(user_config.agent.max_turns),
1290 system_prompt: system.unwrap_or_else(|| user_config.agent.system_prompt.clone()),
1291 timeout_secs: user_config.terminal.timeout,
1292 yolo,
1293 max_context_tokens: 128_000,
1294 streaming: user_config.display.streaming,
1295 };
1296
1297 let agent = Agent::new(provider_box, registry, session_store, agent_config, model.clone());
1299
1300 if let Some(q) = query {
1301 let response = ChatRepl::run_query(agent, &q)
1303 .await
1304 .map_err(|e| anyhow::anyhow!("Query failed: {}", e))?;
1305 println!("{}", response);
1306 } else {
1307 if !quiet {
1309 hermes_agent_runtime::display::print_banner(env!("CARGO_PKG_VERSION"), &model, provider_str);
1310 }
1311
1312 let mut repl =
1313 ChatRepl::new(agent).map_err(|e| anyhow::anyhow!("Failed to create REPL: {}", e))?;
1314
1315 use std::io::Write;
1317 use tokio::io::{AsyncBufReadExt, BufReader};
1318
1319 let stdin = BufReader::new(tokio::io::stdin());
1320 let mut lines = stdin.lines();
1321
1322 loop {
1323 if !quiet {
1324 print!("> ");
1325 let _ = std::io::stdout().flush();
1326 }
1327
1328 let line = tokio::select! {
1330 result = lines.next_line() => {
1331 match result {
1332 Ok(Some(line)) => line,
1333 Ok(None) => break, Err(e) => anyhow::bail!("Input error: {}", e),
1335 }
1336 }
1337 _ = tokio::signal::ctrl_c() => {
1338 println!("\nInterrupted. Saving session...");
1339 let session_id = repl.graceful_shutdown();
1340 println!("Session {} saved. Goodbye!", session_id);
1341 std::process::exit(0);
1342 }
1343 };
1344
1345 let input = line.trim();
1346 if input.is_empty() {
1347 continue;
1348 }
1349
1350 if input.starts_with("/skill") {
1352 let skill_args = input.strip_prefix("/skill").unwrap_or("").trim();
1353 match handle_skill_command(skill_args, &mut repl) {
1354 Ok(Some(msg)) => println!("{}", msg),
1355 Ok(None) => break,
1356 Err(e) => eprintln!("Skill error: {}", e),
1357 }
1358 continue;
1359 }
1360
1361 let turn_result = tokio::select! {
1363 result = repl.run_turn(input) => result,
1364 _ = tokio::signal::ctrl_c() => {
1365 println!("\nInterrupted. Saving session...");
1366 let session_id = repl.graceful_shutdown();
1367 println!("Session {} saved. Goodbye!", session_id);
1368 std::process::exit(0);
1369 }
1370 };
1371
1372 match turn_result {
1373 Ok(response) => {
1374 println!("{}", response.content);
1375 if let Some(ref usage) = response.token_usage {
1377 let cost = hermes_common::model_metadata::estimate_cost(
1378 &model,
1379 usage.input_tokens,
1380 usage.output_tokens,
1381 );
1382 hermes_agent_runtime::display::print_turn_usage(
1383 usage.input_tokens,
1384 usage.output_tokens,
1385 cost,
1386 &model,
1387 );
1388 }
1389 }
1390 Err(e) => {
1391 let msg = e.to_string();
1392 if msg.contains("REPL exited") {
1393 if !quiet {
1394 hermes_agent_runtime::display::print_session_summary(
1395 repl.agent().turns_used(),
1396 0,
1397 repl.agent().total_cost(),
1398 0,
1399 );
1400 let session_id = repl.graceful_shutdown();
1401 println!("Session {} saved. Goodbye!", session_id);
1402 }
1403 break;
1404 }
1405 eprintln!("Error: {}", msg);
1406 }
1407 }
1408 }
1409 }
1410
1411 Ok(())
1412}
1413
1414#[cfg(test)]
1417mod tests {
1418 use super::*;
1419
1420 #[test]
1422 fn test_cli_parse_chat() {
1423 let cli = Cli::parse_from(vec!["hermes", "chat", "gpt-4"]);
1424 if let Commands::Chat { model, .. } = cli.command.unwrap() {
1425 assert_eq!(model, Some("gpt-4".to_string()));
1426 } else {
1427 panic!("expected Chat");
1428 }
1429 }
1430
1431 #[test]
1432 fn test_cli_parse_chat_with_system() {
1433 let cli = Cli::parse_from(vec!["hermes", "chat", "gpt-4", "--system", "You are helpful"]);
1434 if let Commands::Chat { model, system, .. } = cli.command.unwrap() {
1435 assert_eq!(model, Some("gpt-4".to_string()));
1436 assert_eq!(system, Some("You are helpful".to_string()));
1437 } else {
1438 panic!("expected Chat");
1439 }
1440 }
1441
1442 #[test]
1443 fn test_cli_parse_chat_with_query() {
1444 let cli = Cli::parse_from(vec!["hermes", "chat", "-q", "hello world"]);
1445 if let Commands::Chat { query, .. } = cli.command.unwrap() {
1446 assert_eq!(query, Some("hello world".to_string()));
1447 } else {
1448 panic!("expected Chat");
1449 }
1450 }
1451
1452 #[test]
1453 fn test_cli_parse_chat_with_provider() {
1454 let cli = Cli::parse_from(vec!["hermes", "chat", "--provider", "anthropic"]);
1455 if let Commands::Chat { provider, .. } = cli.command.unwrap() {
1456 assert_eq!(provider, Some("anthropic".to_string()));
1457 } else {
1458 panic!("expected Chat");
1459 }
1460 }
1461
1462 #[test]
1463 fn test_cli_parse_chat_with_toolsets() {
1464 let cli = Cli::parse_from(vec!["hermes", "chat", "--toolsets", "web,memory"]);
1465 if let Commands::Chat { toolsets, .. } = cli.command.unwrap() {
1466 assert_eq!(toolsets, Some("web,memory".to_string()));
1467 } else {
1468 panic!("expected Chat");
1469 }
1470 }
1471
1472 #[test]
1473 fn test_cli_parse_chat_yolo() {
1474 let cli = Cli::parse_from(vec!["hermes", "chat", "--yolo"]);
1475 if let Commands::Chat { yolo, .. } = cli.command.unwrap() {
1476 assert!(yolo);
1477 } else {
1478 panic!("expected Chat");
1479 }
1480 }
1481
1482 #[test]
1483 fn test_cli_parse_auth_add() {
1484 let cli =
1485 Cli::parse_from(vec!["hermes", "auth", "add", "openai", "--api-key", "sk-test123"]);
1486 if let Commands::Auth(AuthCommand::Add { provider, api_key, .. }) = cli.command.unwrap() {
1487 assert_eq!(provider, "openai");
1488 assert_eq!(api_key, Some("sk-test123".to_string()));
1489 } else {
1490 panic!("expected Auth::Add");
1491 }
1492 }
1493
1494 #[test]
1495 fn test_cli_parse_auth_add_with_base_url() {
1496 let cli = Cli::parse_from(vec![
1497 "hermes",
1498 "auth",
1499 "add",
1500 "custom",
1501 "--api-key",
1502 "key123",
1503 "--base-url",
1504 "https://api.example.com",
1505 ]);
1506 if let Commands::Auth(AuthCommand::Add { provider, api_key, base_url, .. }) =
1507 cli.command.unwrap()
1508 {
1509 assert_eq!(provider, "custom");
1510 assert_eq!(api_key, Some("key123".to_string()));
1511 assert_eq!(base_url, Some("https://api.example.com".to_string()));
1512 } else {
1513 panic!("expected Auth::Add");
1514 }
1515 }
1516
1517 #[test]
1518 fn test_cli_parse_auth_add_with_type() {
1519 let cli = Cli::parse_from(vec!["hermes", "auth", "add", "nous", "--type", "oauth"]);
1520 if let Commands::Auth(AuthCommand::Add { provider, auth_type, .. }) = cli.command.unwrap() {
1521 assert_eq!(provider, "nous");
1522 assert_eq!(auth_type, Some("oauth".to_string()));
1523 } else {
1524 panic!("expected Auth::Add");
1525 }
1526 }
1527
1528 #[test]
1529 fn test_cli_parse_auth_list() {
1530 let cli = Cli::parse_from(vec!["hermes", "auth", "list"]);
1531 if let Commands::Auth(AuthCommand::List { provider }) = cli.command.unwrap() {
1532 assert!(provider.is_none());
1533 } else {
1534 panic!("expected Auth::List");
1535 }
1536 }
1537
1538 #[test]
1539 fn test_cli_parse_auth_list_with_provider() {
1540 let cli = Cli::parse_from(vec!["hermes", "auth", "list", "openai"]);
1541 if let Commands::Auth(AuthCommand::List { provider }) = cli.command.unwrap() {
1542 assert_eq!(provider, Some("openai".to_string()));
1543 } else {
1544 panic!("expected Auth::List");
1545 }
1546 }
1547
1548 #[test]
1549 fn test_cli_parse_auth_remove() {
1550 let cli = Cli::parse_from(vec!["hermes", "auth", "remove", "openai"]);
1551 if let Commands::Auth(AuthCommand::Remove { provider, .. }) = cli.command.unwrap() {
1552 assert_eq!(provider, "openai");
1553 } else {
1554 panic!("expected Auth::Remove");
1555 }
1556 }
1557
1558 #[test]
1559 fn test_cli_parse_auth_reset() {
1560 let cli = Cli::parse_from(vec!["hermes", "auth", "reset"]);
1561 assert!(matches!(
1562 cli.command.unwrap(),
1563 Commands::Auth(AuthCommand::Reset { provider: None })
1564 ));
1565 }
1566
1567 #[test]
1569 fn test_cli_parse_model_current() {
1570 let cli = Cli::parse_from(vec!["hermes", "model", "--current"]);
1571 if let Commands::Model { current, global, model, .. } = cli.command.unwrap() {
1572 assert!(current);
1573 assert!(!global);
1574 assert_eq!(model, None);
1575 } else {
1576 panic!("expected Model");
1577 }
1578 }
1579
1580 #[test]
1581 fn test_cli_parse_model_global() {
1582 let cli = Cli::parse_from(vec!["hermes", "model", "--global", "claude-3"]);
1583 if let Commands::Model { current, global, model, .. } = cli.command.unwrap() {
1584 assert!(!current);
1585 assert!(global);
1586 assert_eq!(model, Some("claude-3".to_string()));
1587 } else {
1588 panic!("expected Model");
1589 }
1590 }
1591
1592 #[test]
1593 fn test_cli_parse_model_session() {
1594 let cli = Cli::parse_from(vec!["hermes", "model", "gpt-4o"]);
1595 if let Commands::Model { current, global, model, .. } = cli.command.unwrap() {
1596 assert!(!current);
1597 assert!(!global);
1598 assert_eq!(model, Some("gpt-4o".to_string()));
1599 } else {
1600 panic!("expected Model");
1601 }
1602 }
1603
1604 #[test]
1606 fn test_cli_parse_tools_list() {
1607 let cli = Cli::parse_from(vec!["hermes", "tools", "list"]);
1608 if let Commands::Tools(ToolsCommand::List { all, platform }) = cli.command.unwrap() {
1609 assert!(!all);
1610 assert_eq!(platform, "cli");
1611 } else {
1612 panic!("expected Tools::List");
1613 }
1614 }
1615
1616 #[test]
1617 fn test_cli_parse_tools_list_all() {
1618 let cli =
1619 Cli::parse_from(vec!["hermes", "tools", "list", "--all", "--platform", "telegram"]);
1620 if let Commands::Tools(ToolsCommand::List { all, platform }) = cli.command.unwrap() {
1621 assert!(all);
1622 assert_eq!(platform, "telegram");
1623 } else {
1624 panic!("expected Tools::List");
1625 }
1626 }
1627
1628 #[test]
1629 fn test_cli_parse_tools_disable() {
1630 let cli = Cli::parse_from(vec!["hermes", "tools", "disable", "web_search", "memory"]);
1631 if let Commands::Tools(ToolsCommand::Disable { names, platform }) = cli.command.unwrap() {
1632 assert_eq!(names, vec!["web_search", "memory"]);
1633 assert_eq!(platform, "cli");
1634 } else {
1635 panic!("expected Tools::Disable");
1636 }
1637 }
1638
1639 #[test]
1640 fn test_cli_parse_tools_enable() {
1641 let cli = Cli::parse_from(vec![
1642 "hermes",
1643 "tools",
1644 "enable",
1645 "web_search",
1646 "--platform",
1647 "discord",
1648 ]);
1649 if let Commands::Tools(ToolsCommand::Enable { names, platform }) = cli.command.unwrap() {
1650 assert_eq!(names, vec!["web_search"]);
1651 assert_eq!(platform, "discord");
1652 } else {
1653 panic!("expected Tools::Enable");
1654 }
1655 }
1656
1657 #[test]
1659 fn test_cli_parse_skills_search() {
1660 let cli = Cli::parse_from(vec!["hermes", "skills", "search", "web"]);
1661 if let Commands::Skills(SkillsCommand::Search { query, source, limit, .. }) =
1662 cli.command.unwrap()
1663 {
1664 assert_eq!(query, Some("web".to_string()));
1665 assert_eq!(source, "all");
1666 assert_eq!(limit, 10);
1667 } else {
1668 panic!("expected Skills::Search");
1669 }
1670 }
1671
1672 #[test]
1673 fn test_cli_parse_skills_browse() {
1674 let cli = Cli::parse_from(vec!["hermes", "skills", "browse"]);
1675 assert!(matches!(cli.command.unwrap(), Commands::Skills(SkillsCommand::Browse { .. })));
1676 }
1677
1678 #[test]
1679 fn test_cli_parse_skills_inspect() {
1680 let cli = Cli::parse_from(vec!["hermes", "skills", "inspect", "web-search"]);
1681 if let Commands::Skills(SkillsCommand::Inspect { name }) = cli.command.unwrap() {
1682 assert_eq!(name, "web-search");
1683 } else {
1684 panic!("expected Skills::Inspect");
1685 }
1686 }
1687
1688 #[test]
1689 fn test_cli_parse_skills_install() {
1690 let cli = Cli::parse_from(vec![
1691 "hermes",
1692 "skills",
1693 "install",
1694 "openai/skills/skill-creator",
1695 "--force",
1696 ]);
1697 if let Commands::Skills(SkillsCommand::Install { identifier, force, .. }) =
1698 cli.command.unwrap()
1699 {
1700 assert_eq!(identifier, "openai/skills/skill-creator");
1701 assert!(force);
1702 } else {
1703 panic!("expected Skills::Install");
1704 }
1705 }
1706
1707 #[test]
1708 fn test_cli_parse_skills_list() {
1709 let cli = Cli::parse_from(vec!["hermes", "skills", "list", "--source", "hub"]);
1710 if let Commands::Skills(SkillsCommand::List { source }) = cli.command.unwrap() {
1711 assert_eq!(source, "hub");
1712 } else {
1713 panic!("expected Skills::List");
1714 }
1715 }
1716
1717 #[test]
1718 fn test_cli_parse_skills_check() {
1719 assert!(matches!(
1720 Cli::parse_from(vec!["hermes", "skills", "check"]).command.unwrap(),
1721 Commands::Skills(SkillsCommand::Check { .. })
1722 ));
1723 }
1724 #[test]
1725 fn test_cli_parse_skills_update() {
1726 assert!(matches!(
1727 Cli::parse_from(vec!["hermes", "skills", "update"]).command.unwrap(),
1728 Commands::Skills(SkillsCommand::Update { .. })
1729 ));
1730 }
1731 #[test]
1732 fn test_cli_parse_skills_audit() {
1733 assert!(matches!(
1734 Cli::parse_from(vec!["hermes", "skills", "audit"]).command.unwrap(),
1735 Commands::Skills(SkillsCommand::Audit { .. })
1736 ));
1737 }
1738 #[test]
1739 fn test_cli_parse_skills_uninstall() {
1740 assert!(matches!(
1741 Cli::parse_from(vec!["hermes", "skills", "uninstall", "foo"]).command.unwrap(),
1742 Commands::Skills(SkillsCommand::Uninstall { .. })
1743 ));
1744 }
1745
1746 #[test]
1748 fn test_cli_parse_gateway_run() {
1749 let cli = Cli::parse_from(vec!["hermes", "gateway", "run"]);
1750 if let Commands::Gateway(GatewayCommand::Run { platform, .. }) = cli.command.unwrap() {
1751 assert_eq!(platform, None);
1752 } else {
1753 panic!("expected Gateway::Run");
1754 }
1755 }
1756
1757 #[test]
1758 fn test_cli_parse_gateway_run_with_platform() {
1759 let cli = Cli::parse_from(vec!["hermes", "gateway", "run", "-P", "telegram"]);
1760 if let Commands::Gateway(GatewayCommand::Run { platform, .. }) = cli.command.unwrap() {
1761 assert_eq!(platform, Some("telegram".to_string()));
1762 } else {
1763 panic!("expected Gateway::Run");
1764 }
1765 }
1766
1767 #[test]
1768 fn test_cli_parse_gateway_start() {
1769 assert!(matches!(
1770 Cli::parse_from(vec!["hermes", "gateway", "start"]).command.unwrap(),
1771 Commands::Gateway(GatewayCommand::Start { .. })
1772 ));
1773 }
1774 #[test]
1775 fn test_cli_parse_gateway_stop() {
1776 assert!(matches!(
1777 Cli::parse_from(vec!["hermes", "gateway", "stop"]).command.unwrap(),
1778 Commands::Gateway(GatewayCommand::Stop { .. })
1779 ));
1780 }
1781 #[test]
1782 fn test_cli_parse_gateway_restart() {
1783 assert!(matches!(
1784 Cli::parse_from(vec!["hermes", "gateway", "restart"]).command.unwrap(),
1785 Commands::Gateway(GatewayCommand::Restart { .. })
1786 ));
1787 }
1788 #[test]
1789 fn test_cli_parse_gateway_status() {
1790 assert!(matches!(
1791 Cli::parse_from(vec!["hermes", "gateway", "status"]).command.unwrap(),
1792 Commands::Gateway(GatewayCommand::Status { .. })
1793 ));
1794 }
1795 #[test]
1796 fn test_cli_parse_gateway_setup() {
1797 let cli = Cli::parse_from(vec!["hermes", "gateway", "setup", "telegram"]);
1798 if let Commands::Gateway(GatewayCommand::Setup { platform }) = cli.command.unwrap() {
1799 assert_eq!(platform, Some("telegram".to_string()));
1800 } else {
1801 panic!("expected Gateway::Setup");
1802 }
1803 }
1804 #[test]
1805 fn test_cli_parse_gateway_install() {
1806 assert!(matches!(
1807 Cli::parse_from(vec!["hermes", "gateway", "install"]).command.unwrap(),
1808 Commands::Gateway(GatewayCommand::Install { .. })
1809 ));
1810 }
1811 #[test]
1812 fn test_cli_parse_gateway_uninstall() {
1813 assert!(matches!(
1814 Cli::parse_from(vec!["hermes", "gateway", "uninstall"]).command.unwrap(),
1815 Commands::Gateway(GatewayCommand::Uninstall { .. })
1816 ));
1817 }
1818
1819 #[test]
1821 fn test_cli_parse_cron_list() {
1822 assert!(matches!(
1823 Cli::parse_from(vec!["hermes", "cron", "list"]).command.unwrap(),
1824 Commands::Cron(CronCommand::List { .. })
1825 ));
1826 }
1827 #[test]
1828 fn test_cli_parse_cron_add() {
1829 let cli = Cli::parse_from(vec!["hermes", "cron", "add", "every 30m", "check status"]);
1830 if let Commands::Cron(CronCommand::Add { schedule, command, .. }) = cli.command.unwrap() {
1831 assert_eq!(schedule, "every 30m");
1832 assert_eq!(command, Some("check status".to_string()));
1833 } else {
1834 panic!("expected Cron::Add");
1835 }
1836 }
1837 #[test]
1838 fn test_cli_parse_cron_remove() {
1839 assert!(matches!(
1840 Cli::parse_from(vec!["hermes", "cron", "remove", "abc123"]).command.unwrap(),
1841 Commands::Cron(CronCommand::Remove { .. })
1842 ));
1843 }
1844 #[test]
1845 fn test_cli_parse_cron_pause() {
1846 assert!(matches!(
1847 Cli::parse_from(vec!["hermes", "cron", "pause", "abc123"]).command.unwrap(),
1848 Commands::Cron(CronCommand::Pause { .. })
1849 ));
1850 }
1851 #[test]
1852 fn test_cli_parse_cron_resume() {
1853 assert!(matches!(
1854 Cli::parse_from(vec!["hermes", "cron", "resume", "abc123"]).command.unwrap(),
1855 Commands::Cron(CronCommand::Resume { .. })
1856 ));
1857 }
1858 #[test]
1859 fn test_cli_parse_cron_run() {
1860 assert!(matches!(
1861 Cli::parse_from(vec!["hermes", "cron", "run", "abc123"]).command.unwrap(),
1862 Commands::Cron(CronCommand::Run { .. })
1863 ));
1864 }
1865 #[test]
1866 fn test_cli_parse_cron_status() {
1867 assert!(matches!(
1868 Cli::parse_from(vec!["hermes", "cron", "status"]).command.unwrap(),
1869 Commands::Cron(CronCommand::Status)
1870 ));
1871 }
1872 #[test]
1873 fn test_cli_parse_cron_tick() {
1874 assert!(matches!(
1875 Cli::parse_from(vec!["hermes", "cron", "tick"]).command.unwrap(),
1876 Commands::Cron(CronCommand::Tick)
1877 ));
1878 }
1879 #[test]
1880 fn test_cli_parse_cron_edit() {
1881 assert!(matches!(
1882 Cli::parse_from(vec!["hermes", "cron", "edit", "abc123"]).command.unwrap(),
1883 Commands::Cron(CronCommand::Edit { .. })
1884 ));
1885 }
1886
1887 #[test]
1889 fn test_cli_parse_config_show() {
1890 assert!(matches!(
1891 Cli::parse_from(vec!["hermes", "config", "show"]).command.unwrap(),
1892 Commands::Config(ConfigCommand::Show)
1893 ));
1894 }
1895 #[test]
1896 fn test_cli_parse_config_get() {
1897 let cli = Cli::parse_from(vec!["hermes", "config", "get", "model.default"]);
1898 if let Commands::Config(ConfigCommand::Get { key }) = cli.command.unwrap() {
1899 assert_eq!(key, "model.default");
1900 } else {
1901 panic!("expected Config::Get");
1902 }
1903 }
1904 #[test]
1905 fn test_cli_parse_config_set() {
1906 let cli = Cli::parse_from(vec!["hermes", "config", "set", "model.default", "gpt-4"]);
1907 if let Commands::Config(ConfigCommand::Set { key, value }) = cli.command.unwrap() {
1908 assert_eq!(key, "model.default");
1909 assert_eq!(value, "gpt-4");
1910 } else {
1911 panic!("expected Config::Set");
1912 }
1913 }
1914 #[test]
1915 fn test_cli_parse_config_reset() {
1916 assert!(matches!(
1917 Cli::parse_from(vec!["hermes", "config", "reset"]).command.unwrap(),
1918 Commands::Config(ConfigCommand::Reset)
1919 ));
1920 }
1921 #[test]
1922 fn test_cli_parse_config_edit() {
1923 assert!(matches!(
1924 Cli::parse_from(vec!["hermes", "config", "edit"]).command.unwrap(),
1925 Commands::Config(ConfigCommand::Edit)
1926 ));
1927 }
1928 #[test]
1929 fn test_cli_parse_config_path() {
1930 assert!(matches!(
1931 Cli::parse_from(vec!["hermes", "config", "path"]).command.unwrap(),
1932 Commands::Config(ConfigCommand::Path)
1933 ));
1934 }
1935 #[test]
1936 fn test_cli_parse_config_env_path() {
1937 assert!(matches!(
1938 Cli::parse_from(vec!["hermes", "config", "env-path"]).command.unwrap(),
1939 Commands::Config(ConfigCommand::EnvPath)
1940 ));
1941 }
1942 #[test]
1943 fn test_cli_parse_config_check() {
1944 assert!(matches!(
1945 Cli::parse_from(vec!["hermes", "config", "check"]).command.unwrap(),
1946 Commands::Config(ConfigCommand::Check)
1947 ));
1948 }
1949 #[test]
1950 fn test_cli_parse_config_migrate() {
1951 assert!(matches!(
1952 Cli::parse_from(vec!["hermes", "config", "migrate"]).command.unwrap(),
1953 Commands::Config(ConfigCommand::Migrate)
1954 ));
1955 }
1956
1957 #[test]
1959 fn test_cli_parse_setup() {
1960 let cli = Cli::parse_from(vec!["hermes", "setup"]);
1961 if let Commands::Setup { skip_auth, skip_model, .. } = cli.command.unwrap() {
1962 assert!(!skip_auth);
1963 assert!(!skip_model);
1964 } else {
1965 panic!("expected Setup");
1966 }
1967 }
1968 #[test]
1969 fn test_cli_parse_setup_skip_auth() {
1970 let cli = Cli::parse_from(vec!["hermes", "setup", "--skip-auth"]);
1971 if let Commands::Setup { skip_auth, skip_model, .. } = cli.command.unwrap() {
1972 assert!(skip_auth);
1973 assert!(!skip_model);
1974 } else {
1975 panic!("expected Setup");
1976 }
1977 }
1978 #[test]
1979 fn test_cli_parse_setup_section() {
1980 let cli = Cli::parse_from(vec!["hermes", "setup", "gateway"]);
1981 if let Commands::Setup { section, .. } = cli.command.unwrap() {
1982 assert_eq!(section, Some("gateway".to_string()));
1983 } else {
1984 panic!("expected Setup");
1985 }
1986 }
1987
1988 #[test]
1989 fn test_cli_parse_doctor() {
1990 let cli = Cli::parse_from(vec!["hermes", "doctor"]);
1991 if let Commands::Doctor { all, check, .. } = cli.command.unwrap() {
1992 assert!(!all);
1993 assert_eq!(check, None);
1994 } else {
1995 panic!("expected Doctor");
1996 }
1997 }
1998 #[test]
1999 fn test_cli_parse_doctor_fix() {
2000 let cli = Cli::parse_from(vec!["hermes", "doctor", "--fix"]);
2001 if let Commands::Doctor { fix, .. } = cli.command.unwrap() {
2002 assert!(fix);
2003 } else {
2004 panic!("expected Doctor");
2005 }
2006 }
2007
2008 #[test]
2010 fn test_cli_parse_status() {
2011 let cli = Cli::parse_from(vec!["hermes", "status"]);
2012 if let Commands::Status { all, deep } = cli.command.unwrap() {
2013 assert!(!all);
2014 assert!(!deep);
2015 } else {
2016 panic!("expected Status");
2017 }
2018 }
2019 #[test]
2020 fn test_cli_parse_status_all() {
2021 let cli = Cli::parse_from(vec!["hermes", "status", "--all", "--deep"]);
2022 if let Commands::Status { all, deep } = cli.command.unwrap() {
2023 assert!(all);
2024 assert!(deep);
2025 } else {
2026 panic!("expected Status");
2027 }
2028 }
2029
2030 #[test]
2032 fn test_cli_parse_sessions_list() {
2033 assert!(matches!(
2034 Cli::parse_from(vec!["hermes", "sessions", "list"]).command.unwrap(),
2035 Commands::Sessions(SessionsCommand::List { .. })
2036 ));
2037 }
2038 #[test]
2039 fn test_cli_parse_sessions_export() {
2040 assert!(matches!(
2041 Cli::parse_from(vec!["hermes", "sessions", "export", "out.json"]).command.unwrap(),
2042 Commands::Sessions(SessionsCommand::Export { .. })
2043 ));
2044 }
2045 #[test]
2046 fn test_cli_parse_sessions_delete() {
2047 assert!(matches!(
2048 Cli::parse_from(vec!["hermes", "sessions", "delete", "abc123"]).command.unwrap(),
2049 Commands::Sessions(SessionsCommand::Delete { .. })
2050 ));
2051 }
2052 #[test]
2053 fn test_cli_parse_sessions_prune() {
2054 assert!(matches!(
2055 Cli::parse_from(vec!["hermes", "sessions", "prune"]).command.unwrap(),
2056 Commands::Sessions(SessionsCommand::Prune { .. })
2057 ));
2058 }
2059 #[test]
2060 fn test_cli_parse_sessions_stats() {
2061 assert!(matches!(
2062 Cli::parse_from(vec!["hermes", "sessions", "stats"]).command.unwrap(),
2063 Commands::Sessions(SessionsCommand::Stats)
2064 ));
2065 }
2066 #[test]
2067 fn test_cli_parse_sessions_rename() {
2068 assert!(matches!(
2069 Cli::parse_from(vec!["hermes", "sessions", "rename", "abc123", "My", "Session"])
2070 .command
2071 .unwrap(),
2072 Commands::Sessions(SessionsCommand::Rename { .. })
2073 ));
2074 }
2075 #[test]
2076 fn test_cli_parse_sessions_browse() {
2077 assert!(matches!(
2078 Cli::parse_from(vec!["hermes", "sessions", "browse"]).command.unwrap(),
2079 Commands::Sessions(SessionsCommand::Browse { .. })
2080 ));
2081 }
2082
2083 #[test]
2085 fn test_cli_parse_logs() {
2086 let cli = Cli::parse_from(vec!["hermes", "logs"]);
2087 if let Commands::Logs { log_name, lines, .. } = cli.command.unwrap() {
2088 assert_eq!(log_name, None);
2089 assert_eq!(lines, 50);
2090 } else {
2091 panic!("expected Logs");
2092 }
2093 }
2094 #[test]
2095 fn test_cli_parse_logs_with_options() {
2096 let cli = Cli::parse_from(vec![
2097 "hermes", "logs", "errors", "--lines", "100", "-f", "--level", "WARNING",
2098 ]);
2099 if let Commands::Logs { log_name, lines, follow, level, .. } = cli.command.unwrap() {
2100 assert_eq!(log_name, Some("errors".to_string()));
2101 assert_eq!(lines, 100);
2102 assert!(follow);
2103 assert_eq!(level, Some("WARNING".to_string()));
2104 } else {
2105 panic!("expected Logs");
2106 }
2107 }
2108
2109 #[test]
2111 fn test_cli_parse_profile_list() {
2112 assert!(matches!(
2113 Cli::parse_from(vec!["hermes", "profile", "list"]).command.unwrap(),
2114 Commands::Profile(ProfileCommand::List)
2115 ));
2116 }
2117 #[test]
2118 fn test_cli_parse_profile_use() {
2119 assert!(matches!(
2120 Cli::parse_from(vec!["hermes", "profile", "use", "work"]).command.unwrap(),
2121 Commands::Profile(ProfileCommand::Use { .. })
2122 ));
2123 }
2124 #[test]
2125 fn test_cli_parse_profile_create() {
2126 assert!(matches!(
2127 Cli::parse_from(vec!["hermes", "profile", "create", "test"]).command.unwrap(),
2128 Commands::Profile(ProfileCommand::Create { .. })
2129 ));
2130 }
2131 #[test]
2132 fn test_cli_parse_profile_delete() {
2133 assert!(matches!(
2134 Cli::parse_from(vec!["hermes", "profile", "delete", "test"]).command.unwrap(),
2135 Commands::Profile(ProfileCommand::Delete { .. })
2136 ));
2137 }
2138
2139 #[test]
2141 fn test_cli_parse_mcp_serve() {
2142 assert!(matches!(
2143 Cli::parse_from(vec!["hermes", "mcp", "serve"]).command.unwrap(),
2144 Commands::Mcp(McpCommand::Serve { .. })
2145 ));
2146 }
2147 #[test]
2148 fn test_cli_parse_mcp_add() {
2149 assert!(matches!(
2150 Cli::parse_from(vec!["hermes", "mcp", "add", "github"]).command.unwrap(),
2151 Commands::Mcp(McpCommand::Add { .. })
2152 ));
2153 }
2154 #[test]
2155 fn test_cli_parse_mcp_remove() {
2156 assert!(matches!(
2157 Cli::parse_from(vec!["hermes", "mcp", "remove", "github"]).command.unwrap(),
2158 Commands::Mcp(McpCommand::Remove { .. })
2159 ));
2160 }
2161 #[test]
2162 fn test_cli_parse_mcp_list() {
2163 assert!(matches!(
2164 Cli::parse_from(vec!["hermes", "mcp", "list"]).command.unwrap(),
2165 Commands::Mcp(McpCommand::List)
2166 ));
2167 }
2168
2169 #[test]
2171 fn test_cli_parse_memory_setup() {
2172 assert!(matches!(
2173 Cli::parse_from(vec!["hermes", "memory", "setup"]).command.unwrap(),
2174 Commands::Memory(MemoryCommand::Setup)
2175 ));
2176 }
2177 #[test]
2178 fn test_cli_parse_memory_status() {
2179 assert!(matches!(
2180 Cli::parse_from(vec!["hermes", "memory", "status"]).command.unwrap(),
2181 Commands::Memory(MemoryCommand::Status)
2182 ));
2183 }
2184 #[test]
2185 fn test_cli_parse_memory_off() {
2186 assert!(matches!(
2187 Cli::parse_from(vec!["hermes", "memory", "off"]).command.unwrap(),
2188 Commands::Memory(MemoryCommand::Off)
2189 ));
2190 }
2191
2192 #[test]
2194 fn test_cli_parse_webhook_subscribe() {
2195 assert!(matches!(
2196 Cli::parse_from(vec!["hermes", "webhook", "subscribe", "test"]).command.unwrap(),
2197 Commands::Webhook(WebhookCommand::Subscribe { .. })
2198 ));
2199 }
2200 #[test]
2201 fn test_cli_parse_webhook_list() {
2202 assert!(matches!(
2203 Cli::parse_from(vec!["hermes", "webhook", "list"]).command.unwrap(),
2204 Commands::Webhook(WebhookCommand::List)
2205 ));
2206 }
2207
2208 #[test]
2210 fn test_cli_parse_pairing_list() {
2211 assert!(matches!(
2212 Cli::parse_from(vec!["hermes", "pairing", "list"]).command.unwrap(),
2213 Commands::Pairing(PairingCommand::List)
2214 ));
2215 }
2216 #[test]
2217 fn test_cli_parse_pairing_approve() {
2218 assert!(matches!(
2219 Cli::parse_from(vec!["hermes", "pairing", "approve", "telegram", "ABC123"])
2220 .command
2221 .unwrap(),
2222 Commands::Pairing(PairingCommand::Approve { .. })
2223 ));
2224 }
2225
2226 #[test]
2228 fn test_cli_parse_plugins_install() {
2229 assert!(matches!(
2230 Cli::parse_from(vec!["hermes", "plugins", "install", "foo/bar"]).command.unwrap(),
2231 Commands::Plugins(PluginsCommand::Install { .. })
2232 ));
2233 }
2234 #[test]
2235 fn test_cli_parse_plugins_list() {
2236 assert!(matches!(
2237 Cli::parse_from(vec!["hermes", "plugins", "list"]).command.unwrap(),
2238 Commands::Plugins(PluginsCommand::List)
2239 ));
2240 }
2241
2242 #[test]
2244 fn test_cli_parse_backup() {
2245 let cli = Cli::parse_from(vec!["hermes", "backup", "--quick"]);
2246 if let Commands::Backup { quick, .. } = cli.command.unwrap() {
2247 assert!(quick);
2248 } else {
2249 panic!("expected Backup");
2250 }
2251 }
2252 #[test]
2253 fn test_cli_parse_import() {
2254 let cli = Cli::parse_from(vec!["hermes", "import", "backup.zip", "--force"]);
2255 if let Commands::Import { zipfile, force } = cli.command.unwrap() {
2256 assert_eq!(zipfile, "backup.zip");
2257 assert!(force);
2258 } else {
2259 panic!("expected Import");
2260 }
2261 }
2262
2263 #[test]
2265 fn test_cli_parse_debug_share() {
2266 assert!(matches!(
2267 Cli::parse_from(vec!["hermes", "debug", "share"]).command.unwrap(),
2268 Commands::Debug(DebugCommand::Share { .. })
2269 ));
2270 }
2271 #[test]
2272 fn test_cli_parse_dump() {
2273 assert!(matches!(
2274 Cli::parse_from(vec!["hermes", "dump"]).command.unwrap(),
2275 Commands::Dump { .. }
2276 ));
2277 }
2278
2279 #[test]
2281 fn test_cli_parse_completion() {
2282 assert!(matches!(
2283 Cli::parse_from(vec!["hermes", "completion", "bash"]).command.unwrap(),
2284 Commands::Completion { .. }
2285 ));
2286 }
2287 #[test]
2288 fn test_cli_parse_insights() {
2289 assert!(matches!(
2290 Cli::parse_from(vec!["hermes", "insights"]).command.unwrap(),
2291 Commands::Insights { .. }
2292 ));
2293 }
2294
2295 #[test]
2297 fn test_cli_parse_login() {
2298 assert!(matches!(
2299 Cli::parse_from(vec!["hermes", "login"]).command.unwrap(),
2300 Commands::Login { .. }
2301 ));
2302 }
2303 #[test]
2304 fn test_cli_parse_logout() {
2305 assert!(matches!(
2306 Cli::parse_from(vec!["hermes", "logout"]).command.unwrap(),
2307 Commands::Logout { .. }
2308 ));
2309 }
2310
2311 #[test]
2313 fn test_cli_parse_whatsapp() {
2314 assert!(matches!(
2315 Cli::parse_from(vec!["hermes", "whatsapp"]).command.unwrap(),
2316 Commands::Whatsapp
2317 ));
2318 }
2319 #[test]
2320 fn test_cli_parse_acp() {
2321 assert!(matches!(Cli::parse_from(vec!["hermes", "acp"]).command.unwrap(), Commands::Acp));
2322 }
2323 #[test]
2324 fn test_cli_parse_dashboard() {
2325 assert!(matches!(
2326 Cli::parse_from(vec!["hermes", "dashboard"]).command.unwrap(),
2327 Commands::Dashboard { .. }
2328 ));
2329 }
2330
2331 #[test]
2333 fn test_cli_parse_claw_migrate() {
2334 assert!(matches!(
2335 Cli::parse_from(vec!["hermes", "claw", "migrate"]).command.unwrap(),
2336 Commands::Claw(ClawCommand::Migrate { .. })
2337 ));
2338 }
2339 #[test]
2340 fn test_cli_parse_claw_cleanup() {
2341 assert!(matches!(
2342 Cli::parse_from(vec!["hermes", "claw", "cleanup"]).command.unwrap(),
2343 Commands::Claw(ClawCommand::Cleanup { .. })
2344 ));
2345 }
2346
2347 #[test]
2349 fn test_cli_parse_version() {
2350 assert!(matches!(
2351 Cli::parse_from(vec!["hermes", "version"]).command.unwrap(),
2352 Commands::Version
2353 ));
2354 }
2355 #[test]
2356 fn test_cli_parse_update() {
2357 assert!(matches!(
2358 Cli::parse_from(vec!["hermes", "update"]).command.unwrap(),
2359 Commands::Update { .. }
2360 ));
2361 }
2362 #[test]
2363 fn test_cli_parse_update_gateway() {
2364 let cli = Cli::parse_from(vec!["hermes", "update", "--gateway"]);
2365 if let Commands::Update { gateway } = cli.command.unwrap() {
2366 assert!(gateway);
2367 } else {
2368 panic!("expected Update");
2369 }
2370 }
2371 #[test]
2372 fn test_cli_parse_uninstall() {
2373 assert!(matches!(
2374 Cli::parse_from(vec!["hermes", "uninstall"]).command.unwrap(),
2375 Commands::Uninstall { .. }
2376 ));
2377 }
2378 #[test]
2379 fn test_cli_parse_uninstall_full() {
2380 let cli = Cli::parse_from(vec!["hermes", "uninstall", "--full", "--yes"]);
2381 if let Commands::Uninstall { full, yes } = cli.command.unwrap() {
2382 assert!(full);
2383 assert!(yes);
2384 } else {
2385 panic!("expected Uninstall");
2386 }
2387 }
2388
2389 #[test]
2391 fn test_cli_parse_models() {
2392 let cli = Cli::parse_from(vec!["hermes", "models"]);
2393 if let Commands::Models { provider, tools, pricing } = cli.command.unwrap() {
2394 assert!(provider.is_none());
2395 assert!(!tools);
2396 assert!(!pricing);
2397 } else {
2398 panic!("expected Models");
2399 }
2400 }
2401 #[test]
2402 fn test_cli_parse_models_with_provider() {
2403 let cli = Cli::parse_from(vec!["hermes", "models", "--provider", "openai"]);
2404 if let Commands::Models { provider, tools, pricing } = cli.command.unwrap() {
2405 assert_eq!(provider, Some("openai".to_string()));
2406 assert!(!tools);
2407 assert!(!pricing);
2408 } else {
2409 panic!("expected Models");
2410 }
2411 }
2412 #[test]
2413 fn test_cli_parse_models_with_flags() {
2414 let cli = Cli::parse_from(vec!["hermes", "models", "--tools", "--pricing"]);
2415 if let Commands::Models { provider, tools, pricing } = cli.command.unwrap() {
2416 assert!(provider.is_none());
2417 assert!(tools);
2418 assert!(pricing);
2419 } else {
2420 panic!("expected Models");
2421 }
2422 }
2423
2424 #[test]
2426 fn test_cli_parse_verbose() {
2427 let cli = Cli::parse_from(vec!["hermes", "-v", "status"]);
2428 assert!(cli.verbose);
2429 }
2430 #[test]
2431 fn test_cli_parse_debug() {
2432 let cli = Cli::parse_from(vec!["hermes", "-d", "status"]);
2433 assert!(cli.debug);
2434 }
2435 #[test]
2436 fn test_cli_parse_profile() {
2437 let cli = Cli::parse_from(vec!["hermes", "-p", "work", "status"]);
2438 assert_eq!(cli.profile, Some("work".to_string()));
2439 }
2440 #[test]
2441 fn test_cli_parse_resume_global() {
2442 let cli = Cli::parse_from(vec!["hermes", "--resume", "abc123"]);
2443 assert_eq!(cli.resume, Some("abc123".to_string()));
2444 }
2445}