Skip to main content

hermes_agent_cli_core/
lib.rs

1// Hermes CLI Core — full command surface matching Python Hermes CLI
2
3use 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// ── Top-level CLI ────────────────────────────────────────────────────────────
28
29#[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    /// Resume a previous session by ID
41    #[arg(long, global = true, value_name = "SESSION_ID")]
42    resume: Option<String>,
43    /// Resume session by name, or most recent if no name given
44    #[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// ── All Commands ─────────────────────────────────────────────────────────────
51
52#[derive(clap::Subcommand, Debug, Clone)]
53pub enum Commands {
54    // ── Chat (interactive) ───────────────────────────────────────────────
55    Chat {
56        model: Option<String>,
57        /// Single query (non-interactive mode)
58        #[arg(short, long)]
59        query: Option<String>,
60        /// Optional local image path to attach
61        #[arg(long)]
62        image: Option<String>,
63        /// System prompt override
64        #[arg(short, long)]
65        system: Option<String>,
66        /// Comma-separated toolsets to enable
67        #[arg(short, long)]
68        toolsets: Option<String>,
69        /// Preload one or more skills (repeat flag)
70        #[arg(long)]
71        skills: Option<Vec<String>>,
72        /// Inference provider
73        #[arg(long)]
74        provider: Option<String>,
75        /// Verbose output (chat-specific)
76        #[arg(long)]
77        chat_verbose: bool,
78        /// Quiet mode: suppress banner, spinner, tool previews
79        #[arg(short = 'Q', long)]
80        quiet: bool,
81        /// Resume a previous session by ID
82        #[arg(short, long)]
83        resume: Option<String>,
84        /// Resume session by name, or most recent
85        #[arg(short = 'n', long = "continue")]
86        continue_last: Option<Option<String>>,
87        /// Run in isolated git worktree
88        #[arg(short, long)]
89        worktree: bool,
90        /// Enable filesystem checkpoints before destructive ops
91        #[arg(long)]
92        checkpoints: bool,
93        /// Max tool-calling iterations per turn
94        #[arg(long)]
95        max_turns: Option<u32>,
96        /// Bypass all dangerous command approval prompts
97        #[arg(long)]
98        yolo: bool,
99        /// Include session ID in system prompt
100        #[arg(long)]
101        pass_session_id: bool,
102        /// Session source tag for filtering (default: cli)
103        #[arg(long)]
104        source: Option<String>,
105    },
106
107    // ── Auth ─────────────────────────────────────────────────────────────
108    #[command(subcommand)]
109    Auth(AuthCommand),
110
111    // ── Model (interactive selection) ─────────────────────────────────────
112    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    // ── Tools ────────────────────────────────────────────────────────────
137    #[command(subcommand)]
138    Tools(ToolsCommand),
139
140    // ── Skills ───────────────────────────────────────────────────────────
141    #[command(subcommand)]
142    Skills(SkillsCommand),
143
144    // ── Gateway ──────────────────────────────────────────────────────────
145    #[command(subcommand)]
146    Gateway(GatewayCommand),
147
148    // ── Cron ─────────────────────────────────────────────────────────────
149    #[command(subcommand)]
150    Cron(CronCommand),
151
152    // ── Config ───────────────────────────────────────────────────────────
153    #[command(subcommand)]
154    Config(ConfigCommand),
155
156    // ── Setup ────────────────────────────────────────────────────────────
157    Setup {
158        /// Run setup section: model|tts|terminal|gateway|tools|agent
159        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 ────────────────────────────────────────────────────────────
171    Doctor {
172        #[arg(short, long)]
173        all: bool,
174        /// Check a specific component
175        check: Option<String>,
176        #[arg(long)]
177        fix: bool,
178    },
179
180    // ── Status ────────────────────────────────────────────────────────────
181    Status {
182        #[arg(long)]
183        all: bool,
184        #[arg(long)]
185        deep: bool,
186    },
187
188    // ── Sessions ─────────────────────────────────────────────────────────
189    #[command(subcommand)]
190    Sessions(SessionsCommand),
191
192    // ── Logs ──────────────────────────────────────────────────────────────
193    Logs {
194        /// Log to view: agent (default), errors, gateway, or 'list'
195        log_name: Option<String>,
196        /// Number of lines to show
197        #[arg(long, default_value = "50")]
198        lines: u32,
199        /// Follow the log in real time
200        #[arg(short, long)]
201        follow: bool,
202        /// Minimum log level (DEBUG, INFO, WARNING, ERROR)
203        #[arg(long)]
204        level: Option<String>,
205        /// Filter by session ID
206        #[arg(long)]
207        session: Option<String>,
208        /// Show lines since TIME ago (e.g. 1h, 30m, 2d)
209        #[arg(long)]
210        since: Option<String>,
211        /// Filter by component: gateway, agent, tools, cli, cron
212        #[arg(long)]
213        component: Option<String>,
214    },
215
216    // ── Profile ──────────────────────────────────────────────────────────
217    #[command(subcommand)]
218    Profile(ProfileCommand),
219
220    // ── MCP ───────────────────────────────────────────────────────────────
221    #[command(subcommand)]
222    Mcp(McpCommand),
223
224    // ── Memory ───────────────────────────────────────────────────────────
225    #[command(subcommand)]
226    Memory(MemoryCommand),
227
228    // ── Webhook ──────────────────────────────────────────────────────────
229    #[command(subcommand)]
230    Webhook(WebhookCommand),
231
232    // ── Pairing ─────────────────────────────────────────────────────────
233    #[command(subcommand)]
234    Pairing(PairingCommand),
235
236    // ── Plugins ──────────────────────────────────────────────────────────
237    #[command(subcommand)]
238    Plugins(PluginsCommand),
239
240    // ── Backup ───────────────────────────────────────────────────────────
241    Backup {
242        /// Output path for the zip file
243        #[arg(short, long)]
244        output: Option<String>,
245        /// Quick snapshot of critical state files only
246        #[arg(short, long)]
247        quick: bool,
248        /// Label for the snapshot (with --quick)
249        #[arg(short, long)]
250        label: Option<String>,
251    },
252
253    // ── Import ────────────────────────────────────────────────────────────
254    Import {
255        /// Path to the backup zip file
256        zipfile: String,
257        /// Overwrite existing files without confirmation
258        #[arg(short, long)]
259        force: bool,
260    },
261
262    // ── Debug ─────────────────────────────────────────────────────────────
263    #[command(subcommand)]
264    Debug(DebugCommand),
265
266    // ── Dump ─────────────────────────────────────────────────────────────
267    Dump {
268        /// Show redacted API key prefixes
269        #[arg(long)]
270        show_keys: bool,
271    },
272
273    // ── Completion ───────────────────────────────────────────────────────
274    Completion {
275        /// Shell type
276        shell: Option<String>,
277    },
278
279    // ── Insights ─────────────────────────────────────────────────────────
280    Insights {
281        /// Number of days to analyze
282        #[arg(long, default_value = "30")]
283        days: u32,
284        /// Filter by platform source
285        #[arg(long)]
286        source: Option<String>,
287    },
288
289    // ── Login ────────────────────────────────────────────────────────────
290    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 ────────────────────────────────────────────────────────────
312    Logout {
313        #[arg(long)]
314        provider: Option<String>,
315    },
316
317    // ── WhatsApp ─────────────────────────────────────────────────────────
318    Whatsapp,
319
320    // ── ACP ───────────────────────────────────────────────────────────────
321    Acp,
322
323    // ── Dashboard ─────────────────────────────────────────────────────────
324    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    // ── Claw (OpenClaw migration) ────────────────────────────────────────
334    #[command(subcommand)]
335    Claw(ClawCommand),
336
337    // ── Models ────────────────────────────────────────────────────────────
338    Models {
339        /// Filter by provider name
340        #[arg(long)]
341        provider: Option<String>,
342        /// Show only models supporting tool use
343        #[arg(long)]
344        tools: bool,
345        /// Show pricing information
346        #[arg(long)]
347        pricing: bool,
348    },
349
350    // ── Version ──────────────────────────────────────────────────────────
351    Version,
352
353    // ── Update ───────────────────────────────────────────────────────────
354    Update {
355        #[arg(long)]
356        gateway: bool,
357    },
358
359    // ── Uninstall ────────────────────────────────────────────────────────
360    Uninstall {
361        #[arg(long)]
362        full: bool,
363        #[arg(short, long)]
364        yes: bool,
365    },
366}
367
368// ── Subcommand Enums ─────────────────────────────────────────────────────────
369
370#[derive(clap::Subcommand, Debug, Clone)]
371#[allow(clippy::large_enum_variant)]
372pub enum AuthCommand {
373    Add {
374        provider: String,
375        /// API key value (otherwise prompted)
376        #[arg(long)]
377        api_key: Option<String>,
378        /// Credential type: oauth, api-key
379        #[arg(long = "type", value_name = "AUTH_TYPE")]
380        auth_type: Option<String>,
381        /// Optional display label
382        #[arg(long)]
383        label: Option<String>,
384        /// Base URL override
385        #[arg(long)]
386        base_url: Option<String>,
387        /// Nous portal base URL
388        #[arg(long)]
389        portal_url: Option<String>,
390        /// Nous inference base URL
391        #[arg(long)]
392        inference_url: Option<String>,
393        /// OAuth client id
394        #[arg(long)]
395        client_id: Option<String>,
396        /// OAuth scope override
397        #[arg(long)]
398        scope: Option<String>,
399        /// Do not auto-open browser for OAuth
400        #[arg(long)]
401        no_browser: bool,
402        /// OAuth/network timeout in seconds
403        #[arg(long)]
404        timeout: Option<f64>,
405        /// Disable TLS verification
406        #[arg(long)]
407        insecure: bool,
408        /// Custom CA bundle path
409        #[arg(long)]
410        ca_bundle: Option<String>,
411    },
412    List {
413        /// Optional provider filter
414        provider: Option<String>,
415    },
416    Remove {
417        provider: String,
418        /// Credential index, entry id, or exact label
419        target: Option<String>,
420    },
421    Reset {
422        /// Provider to reset exhaustion for
423        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        /// Platform to show (default: cli)
433        #[arg(long, default_value = "cli")]
434        platform: String,
435    },
436    Disable {
437        /// Toolset names to disable
438        names: Vec<String>,
439        #[arg(long, default_value = "cli")]
440        platform: String,
441    },
442    Enable {
443        /// Toolset names to enable
444        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        /// Specific skill to check (default: all)
485        name: Option<String>,
486    },
487    Update {
488        /// Specific skill to update (default: all outdated)
489        name: Option<String>,
490    },
491    Audit {
492        /// Specific skill to audit (default: all)
493        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 JSON file path (use - for stdout)
516        output: String,
517    },
518    Import {
519        /// Input JSON file path
520        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        /// GitHub repo (e.g. owner/repo)
531        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        /// Increase stderr log verbosity
544        #[arg(long)]
545        verbose: bool,
546        /// Suppress all stderr log output
547        #[arg(short, long)]
548        quiet: bool,
549        /// Replace any existing gateway instance
550        #[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        /// Optional self-contained prompt
599        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        /// Route name (used in URL)
796        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
895// ── Entry point ──────────────────────────────────────────────────────────────
896
897/// Check C drive free space and auto-clean when below threshold.
898/// Returns the free GB after any cleanup.
899fn ensure_disk_space(threshold_gb: f64) -> f64 {
900    #[cfg(target_os = "windows")]
901    {
902        // Use PowerShell to check disk space
903        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, // Assume OK if can't check
913        };
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            // Clean cargo target on E: drive
922            let target_dir = std::env::current_dir().unwrap_or_default().join("target");
923            let _ = std::fs::remove_dir_all(&target_dir);
924
925            // Clean C: caches
926            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            // Re-check after cleanup
938            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 // Non-Windows: no-op
968    }
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    // Auto-check C drive space on Windows
977    ensure_disk_space(2.0);
978
979    let _config = Config::load()?;
980
981    // Default to chat if no command specified
982    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        // Stub handlers for new commands
1056        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
1132/// Handle /skill commands using the SkillStore.
1133fn 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/// Handle the `hermes chat` command — wire CLI to runtime
1189#[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    // Load user config (config.yaml)
1213    let user_config = crate::config::Config::load().unwrap_or_default();
1214
1215    // Resolve provider: CLI flag > config > default
1216    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    // Resolve API key: credential pool (round-robin) > env var
1230    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    // Resolve base_url: pool entry > config > provider default
1250    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    // Resolve model: CLI flag > config > provider default
1261    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    // Create tool registry
1272    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    // Create session store using HERMES_HOME
1282    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    // Create agent config: CLI flags override config.yaml
1288    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    // Create agent
1298    let agent = Agent::new(provider_box, registry, session_store, agent_config, model.clone());
1299
1300    if let Some(q) = query {
1301        // Single-shot mode
1302        let response = ChatRepl::run_query(agent, &q)
1303            .await
1304            .map_err(|e| anyhow::anyhow!("Query failed: {}", e))?;
1305        println!("{}", response);
1306    } else {
1307        // Interactive REPL mode
1308        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        // Async REPL loop with Ctrl+C handling
1316        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            // Race between next input line and Ctrl+C
1329            let line = tokio::select! {
1330                result = lines.next_line() => {
1331                    match result {
1332                        Ok(Some(line)) => line,
1333                        Ok(None) => break, // EOF
1334                        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            // Handle /skill commands at CLI level (SkillStore lives in cli-core)
1351            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            // Run the turn, but allow Ctrl+C to cancel long-running LLM calls
1362            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                    // Show per-turn usage if available
1376                    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// ── Tests ────────────────────────────────────────────────────────────────────
1415
1416#[cfg(test)]
1417mod tests {
1418    use super::*;
1419
1420    // === Chat ===
1421    #[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    // === Model ===
1568    #[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    // === Tools ===
1605    #[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    // === Skills ===
1658    #[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    // === Gateway ===
1747    #[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    // === Cron ===
1820    #[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    // === Config ===
1888    #[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    // === Setup / Doctor ===
1958    #[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    // === Status ===
2009    #[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    // === Sessions ===
2031    #[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    // === Logs ===
2084    #[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    // === Profile ===
2110    #[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    // === MCP ===
2140    #[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    // === Memory ===
2170    #[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    // === Webhook ===
2193    #[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    // === Pairing ===
2209    #[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    // === Plugins ===
2227    #[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    // === Backup / Import ===
2243    #[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    // === Debug / Dump ===
2264    #[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    // === Completion / Insights ===
2280    #[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    // === Login / Logout ===
2296    #[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    // === WhatsApp / ACP / Dashboard ===
2312    #[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    // === Claw ===
2332    #[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    // === Version / Update / Uninstall ===
2348    #[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    // === Models ===
2390    #[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    // === Global flags ===
2425    #[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}