Skip to main content

hermes_agent_cli_core/
commands.rs

1use super::{
2    AuthCommand, ClawCommand, ConfigCommand, CronCommand, DebugCommand, GatewayCommand, McpCommand,
3    MemoryCommand, PairingCommand, PluginsCommand, ProfileCommand, SessionsCommand, SkillsCommand,
4    ToolsCommand, WebhookCommand,
5};
6use crate::auth::AuthStore;
7use crate::config::Config;
8use crate::cron as cron_mod;
9use crate::gateway as gateway_mod;
10use crate::pairings::{PairingStatus, PairingStore};
11use crate::plugins::{Plugin, PluginStore};
12use crate::skills::SkillsIndex;
13use crate::tools::{self, ToolsConfig};
14use crate::webhooks::{Webhook, WebhookStore};
15use anyhow::{Context, Result};
16use serde_json;
17use std::fs;
18use std::path::Path;
19use std::path::PathBuf;
20use tracing::info;
21
22pub async fn handle_auth(cmd: AuthCommand) -> Result<()> {
23    match cmd {
24        AuthCommand::Add { provider, api_key, base_url, .. } => {
25            info!("adding auth for provider: {}", provider);
26            if api_key.is_none() {
27                anyhow::bail!(
28                    "API key is required. Use: hermes auth add {} --api-key <KEY>",
29                    provider
30                );
31            }
32            let api_key = api_key.unwrap();
33            if api_key.is_empty() {
34                anyhow::bail!("API key cannot be empty");
35            }
36            let mut store = AuthStore::load()?;
37            store.add(&provider, &api_key, base_url.as_deref());
38            store.save()?;
39            println!("Auth credentials added for {}", provider);
40        }
41        AuthCommand::List { .. } => {
42            info!("listing auth credentials");
43            let store = AuthStore::load()?;
44            let credentials = store.list();
45            if credentials.is_empty() {
46                println!("No auth credentials configured.");
47                println!("Run 'hermes auth add <provider> --api-key <KEY>' to add credentials.");
48            } else {
49                println!("Configured credentials:");
50                for (provider, masked_key, base_url) in credentials {
51                    println!("  {}: {}", provider, masked_key);
52                    if let Some(url) = base_url {
53                        println!("    base_url: {}", url);
54                    }
55                }
56            }
57        }
58        AuthCommand::Remove { provider, .. } => {
59            info!("removing auth for provider: {}", provider);
60            let mut store = AuthStore::load()?;
61            if store.remove(&provider) {
62                store.save()?;
63                println!("Auth credentials removed for {}", provider);
64            } else {
65                println!("No auth credentials found for {}", provider);
66            }
67        }
68        AuthCommand::Reset { .. } => {
69            info!("resetting all auth credentials");
70            let mut store = AuthStore::load()?;
71            let count = store.credentials.len();
72            store.reset();
73            store.save()?;
74            println!("All auth credentials cleared ({} removed).", count);
75        }
76    }
77    Ok(())
78}
79
80pub fn handle_model(current: bool, global: bool, model: Option<&str>) -> Result<()> {
81    let config = Config::load()?;
82    match (current, global, model) {
83        (true, _, _) => {
84            info!("showing current model");
85            // Priority: session env var > global config
86            let session_model = std::env::var("HERMES_SESSION_MODEL").ok();
87            let effective_model = session_model.as_ref().unwrap_or(&config.model.default);
88            println!("Current model: {}", effective_model);
89            if session_model.is_some() {
90                println!("(session override)");
91            }
92            if !config.model.provider.is_empty() {
93                println!("Provider: {}", config.model.provider);
94            }
95            if !config.model.base_url.is_empty() {
96                println!("Base URL: {}", config.model.base_url);
97            }
98        }
99        (_, true, Some(m)) => {
100            info!("setting global default model: {}", m);
101            let mut config = config;
102            config.model.default = m.to_string();
103            config.save()?;
104            println!("Set global default model to: {}", m);
105        }
106        (_, _, Some(m)) => {
107            info!("setting session model: {}", m);
108            // Session model via environment variable - only affects this process and children
109            std::env::set_var("HERMES_SESSION_MODEL", m);
110            println!("Session model set to: {} (expires when shell exits)", m);
111        }
112        _ => {
113            println!("Model command:");
114            println!("  hermes model                    - show current model");
115            println!("  hermes model <name>            - set session model (env var)");
116            println!("  hermes model --global <name>   - set global default model");
117            println!("  hermes model --current          - show current model details");
118        }
119    }
120    Ok(())
121}
122
123pub fn handle_models(provider: Option<&str>, tools_only: bool, show_pricing: bool) -> Result<()> {
124    use hermes_common::model_metadata;
125
126    let all = model_metadata::all_models();
127
128    // Filter by provider if specified
129    let models: Vec<_> = if let Some(p) = provider {
130        let parsed: hermes_common::Provider = p.parse().map_err(|e| anyhow::anyhow!("{}", e))?;
131        model_metadata::list_models_by_provider(&parsed).into_iter().collect()
132    } else {
133        all.iter().collect()
134    };
135
136    // Filter by tool support
137    let models: Vec<_> =
138        if tools_only { models.into_iter().filter(|m| m.supports_tools).collect() } else { models };
139
140    if models.is_empty() {
141        println!("No models found matching the given filters.");
142        return Ok(());
143    }
144
145    // Compute column widths
146    let name_w = models.iter().map(|m| m.name.len()).max().unwrap_or(4).max(4);
147    let prov_w = models.iter().map(|m| m.provider.to_string().len()).max().unwrap_or(8).max(8);
148
149    // Header
150    if show_pricing {
151        println!(
152            "{:<name_w$}  {:<prov_w$}  {:>7}  {:>7}  {:>5}  {:>5}  {:>9}  {:>9}",
153            "Model",
154            "Provider",
155            "Context",
156            "MaxOut",
157            "Vis",
158            "Tools",
159            "$ In/1M",
160            "$ Out/1M",
161            name_w = name_w,
162            prov_w = prov_w,
163        );
164    } else {
165        println!(
166            "{:<name_w$}  {:<prov_w$}  {:>7}  {:>7}  {:>5}  {:>5}",
167            "Model",
168            "Provider",
169            "Context",
170            "MaxOut",
171            "Vis",
172            "Tools",
173            name_w = name_w,
174            prov_w = prov_w,
175        );
176    }
177
178    // Separator
179    let sep_len = if show_pricing {
180        name_w + 2 + prov_w + 2 + 7 + 2 + 7 + 2 + 5 + 2 + 5 + 2 + 9 + 2 + 9
181    } else {
182        name_w + 2 + prov_w + 2 + 7 + 2 + 7 + 2 + 5 + 2 + 5
183    };
184    println!("{}", "-".repeat(sep_len));
185
186    for m in &models {
187        let ctx = format_tokens(m.context_length);
188        let max_out = format_tokens(m.max_output_tokens);
189        let vis = if m.supports_vision { "yes" } else { "no" };
190        let tls = if m.supports_tools { "yes" } else { "no" };
191
192        if show_pricing {
193            let in_price = format_price(m.input_price_per_million);
194            let out_price = format_price(m.output_price_per_million);
195            println!(
196                "{:<name_w$}  {:<prov_w$}  {:>7}  {:>7}  {:>5}  {:>5}  {:>9}  {:>9}",
197                m.name,
198                m.provider,
199                ctx,
200                max_out,
201                vis,
202                tls,
203                in_price,
204                out_price,
205                name_w = name_w,
206                prov_w = prov_w,
207            );
208        } else {
209            println!(
210                "{:<name_w$}  {:<prov_w$}  {:>7}  {:>7}  {:>5}  {:>5}",
211                m.name,
212                m.provider,
213                ctx,
214                max_out,
215                vis,
216                tls,
217                name_w = name_w,
218                prov_w = prov_w,
219            );
220        }
221    }
222
223    println!();
224    println!("{} models shown.", models.len());
225
226    Ok(())
227}
228
229/// Format a token count with K/M suffix for readability.
230fn format_tokens(n: u32) -> String {
231    if n >= 1_000_000 {
232        format!("{:.1}M", n as f64 / 1_000_000.0)
233    } else if n >= 1_000 {
234        format!("{}K", n / 1_000)
235    } else {
236        format!("{}", n)
237    }
238}
239
240/// Format a price per million tokens.
241fn format_price(price: f64) -> String {
242    if price == 0.0 {
243        "free".to_string()
244    } else {
245        format!("${:.2}", price)
246    }
247}
248
249pub fn handle_tools(cmd: ToolsCommand) -> Result<()> {
250    match cmd {
251        ToolsCommand::List { all, .. } => {
252            info!("listing tools (all: {})", all);
253            let tools = tools::list_tools(all)?;
254            if tools.is_empty() {
255                println!("No tools available.");
256            } else {
257                println!("Available tools:");
258                for (name, description, toolset, enabled) in tools {
259                    let status = if enabled { "" } else { " (disabled)" };
260                    println!("  {}: {} [{}{}]", name, description, toolset, status);
261                }
262            }
263        }
264        ToolsCommand::Disable { names, .. } => {
265            for name in &names {
266                info!("disabling tool: {}", name);
267                let mut config = ToolsConfig::load()?;
268                let builtins: Vec<_> =
269                    tools::get_builtin_tools().iter().map(|t| t.name.to_string()).collect();
270                if !builtins.contains(name) {
271                    println!("Warning: '{}' is not a built-in tool.", name);
272                }
273                config.disable(name);
274                config.save()?;
275                println!("Tool '{}' disabled.", name);
276            }
277        }
278        ToolsCommand::Enable { names, .. } => {
279            for name in &names {
280                info!("enabling tool: {}", name);
281                let mut config = ToolsConfig::load()?;
282                config.enable(name);
283                config.save()?;
284                println!("Tool '{}' enabled.", name);
285            }
286        }
287    }
288    Ok(())
289}
290
291pub fn handle_skills(cmd: SkillsCommand) -> Result<()> {
292    match cmd {
293        SkillsCommand::Search { query, .. } => {
294            info!("searching skills: {:?}", query);
295            let mut index = SkillsIndex::load()?;
296            let count = index.scan_local_skills()?;
297
298            let results: Vec<_> = if let Some(ref q) = query {
299                index.search(q).into_iter().cloned().collect()
300            } else {
301                index.get_all().into_iter().cloned().collect()
302            };
303
304            if results.is_empty() {
305                if let Some(query) = &query {
306                    println!("No skills found matching '{}'.", query);
307                } else {
308                    println!("No skills installed.");
309                    println!("Run 'hermes skills install <name>' to install a skill.");
310                }
311            } else {
312                println!("Found {} skill(s):", results.len());
313                for skill in results {
314                    println!("  {}: {}", skill.name, skill.description);
315                    if !skill.tags.is_empty() {
316                        println!("    tags: {}", skill.tags.join(", "));
317                    }
318                }
319            }
320            let _ = count; // suppress unused warning
321        }
322        SkillsCommand::Browse { .. } => {
323            info!("browsing skills hub");
324            println!("Skills Hub:");
325            println!("  Browse installed skills: hermes skills search");
326            println!("  Install from GitHub: hermes skills install <repo>");
327            println!("  Official skills: https://github.com/nousresearch/hermes-skills");
328        }
329        SkillsCommand::Inspect { name } => {
330            info!("inspecting skill: {}", name);
331            let index = SkillsIndex::load()?;
332            if let Some(skill) = index.get(&name) {
333                println!("Skill: {}", skill.name);
334                println!("Description: {}", skill.description);
335                if let Some(version) = &skill.version {
336                    println!("Version: {}", version);
337                }
338                if let Some(license) = &skill.license {
339                    println!("License: {}", license);
340                }
341                if !skill.platforms.is_empty() {
342                    println!("Platforms: {}", skill.platforms.join(", "));
343                }
344                if !skill.tags.is_empty() {
345                    println!("Tags: {}", skill.tags.join(", "));
346                }
347                if !skill.related_skills.is_empty() {
348                    println!("Related: {}", skill.related_skills.join(", "));
349                }
350
351                // Try to show skill path
352                let skills_home = SkillsIndex::skills_home();
353                let skill_path = skills_home.join(&skill.name);
354                if skill_path.exists() {
355                    println!("Location: {:?}", skill_path);
356                }
357            } else {
358                println!(
359                    "Skill '{}' not found. Run 'hermes skills search' to see installed skills.",
360                    name
361                );
362            }
363        }
364        SkillsCommand::Install { identifier, .. } => {
365            info!("installing skill: {}", identifier);
366            if identifier.contains('/') {
367                println!("Skill install from '{}' requested.", identifier);
368                println!("Note: Full remote install requires network access.");
369                println!("For now, skills should be installed manually to ~/.hermes/skills/");
370            } else {
371                println!("Installing skill '{}'...", identifier);
372                println!("Skill '{}' is not available in the registry.", identifier);
373            }
374        }
375        SkillsCommand::Uninstall { name } => {
376            info!("removing skill: {}", name);
377            let mut index = SkillsIndex::load()?;
378            if index.remove(&name) {
379                index.save()?;
380                let skills_home = SkillsIndex::skills_home();
381                let skill_path = skills_home.join(&name);
382                if skill_path.exists() {
383                    println!("Skill '{}' removed from index.", name);
384                    println!("Note: Skill files at {:?} were not deleted.", skill_path);
385                } else {
386                    println!("Skill '{}' removed from index (no files found).", name);
387                }
388            } else {
389                println!("Skill '{}' not found in index.", name);
390            }
391        }
392        SkillsCommand::List { .. } => {
393            info!("listing all skills");
394            let mut index = SkillsIndex::load()?;
395            let count = index.scan_local_skills()?;
396            let all_skills: Vec<_> = index.get_all().into_iter().cloned().collect();
397
398            if all_skills.is_empty() {
399                println!("No skills installed.");
400                println!("Run 'hermes skills install <name>' to install a skill.");
401            } else {
402                println!("Installed Skills ({}):", all_skills.len());
403                for skill in &all_skills {
404                    println!("  {}: {}", skill.name, skill.description);
405                    if !skill.tags.is_empty() {
406                        println!("    tags: {}", skill.tags.join(", "));
407                    }
408                    if let Some(version) = &skill.version {
409                        println!("    version: {}", version);
410                    }
411                }
412            }
413            let _ = count; // suppress unused warning
414        }
415        SkillsCommand::Check { .. } => {
416            info!("checking installed skills");
417            let mut index = SkillsIndex::load()?;
418            let _ = index.scan_local_skills()?;
419            let skills_home = SkillsIndex::skills_home();
420
421            println!("Skills Check");
422            println!("=============");
423            println!();
424
425            if !skills_home.exists() {
426                println!("Skills directory does not exist: {:?}", skills_home);
427                println!("No skills are installed.");
428                return Ok(());
429            }
430
431            let all_skills: Vec<_> = index.get_all().into_iter().cloned().collect();
432
433            if all_skills.is_empty() {
434                println!("No skills found in index.");
435                println!("Run 'hermes skills list' to see installed skills.");
436                return Ok(());
437            }
438
439            let mut issues = 0;
440            for skill in &all_skills {
441                let skill_path = skills_home.join(&skill.name);
442                let mut skill_issues = Vec::new();
443
444                // Check for required SKILL.md
445                if !skill_path.join("SKILL.md").exists() {
446                    skill_issues.push("missing SKILL.md");
447                }
448
449                // Check for required files based on skill type
450                let skill_md_path = skill_path.join("SKILL.md");
451                if skill_md_path.exists() {
452                    if let Ok(_content) = std::fs::read_to_string(&skill_md_path) {
453                        // Check if description is empty in frontmatter
454                        if skill.description.is_empty() {
455                            skill_issues.push("empty description in SKILL.md");
456                        }
457                    }
458                }
459
460                if skill_issues.is_empty() {
461                    println!("  [OK] {}: valid", skill.name);
462                } else {
463                    for issue in &skill_issues {
464                        println!("  [WARN] {}: {}", skill.name, issue);
465                    }
466                    issues += 1;
467                }
468            }
469
470            println!();
471            if issues == 0 {
472                println!("All skills passed validation!");
473            } else {
474                println!("{} skill(s) have warnings.", issues);
475            }
476        }
477        SkillsCommand::Update { .. } => {
478            info!("updating skills");
479            println!("Skills Update");
480            println!("=============");
481            println!();
482            println!("Skills are updated by reinstalling them:");
483            println!();
484            println!("To update a specific skill:");
485            println!("  1. hermes skills uninstall <name>");
486            println!("  2. hermes skills install <name>");
487            println!();
488            println!("To update all skills:");
489            println!("  - Remove the skills directory and reinstall:");
490            println!("    rm -rf ~/.hermes/skills/");
491            println!("    hermes skills install <each-skill>");
492            println!();
493            println!("Note: Skills are manually managed. Automatic updates require");
494            println!("      a skill registry server which is not yet implemented.");
495        }
496        SkillsCommand::Audit { .. } => {
497            info!("auditing skills security");
498            println!("Skills Audit");
499            println!("=============");
500            println!();
501
502            let skills_home = SkillsIndex::skills_home();
503            if !skills_home.exists() {
504                println!("No skills directory found.");
505                return Ok(());
506            }
507
508            let mut index = SkillsIndex::load()?;
509            let _ = index.scan_local_skills()?;
510            let all_skills: Vec<_> = index.get_all().into_iter().cloned().collect();
511
512            if all_skills.is_empty() {
513                println!("No skills installed to audit.");
514                return Ok(());
515            }
516
517            println!("Auditing {} skill(s)...", all_skills.len());
518            println!();
519
520            let mut passed = 0;
521            let mut warnings = 0;
522
523            for skill in &all_skills {
524                let skill_path = skills_home.join(&skill.name);
525                let skill_md = skill_path.join("SKILL.md");
526
527                // Basic security checks
528                let mut issues = Vec::new();
529
530                // Check SKILL.md exists
531                if !skill_md.exists() {
532                    issues.push("missing SKILL.md");
533                }
534
535                // Check for potentially dangerous patterns in skill path
536                if skill.name.contains("..")
537                    || skill.name.contains('/')
538                    || skill.name.contains('\\')
539                {
540                    issues.push("skill name contains path separators");
541                }
542
543                // Check skill description isn't empty (could hide malicious content)
544                if skill.description.is_empty() {
545                    issues.push("empty description");
546                }
547
548                // Check skill is from known sources (has version/license)
549                if skill.version.is_none() {
550                    issues.push("no version specified");
551                }
552
553                if issues.is_empty() {
554                    println!("  [PASS] {}", skill.name);
555                    passed += 1;
556                } else {
557                    for issue in &issues {
558                        println!("  [WARN] {}: {}", skill.name, issue);
559                    }
560                    warnings += 1;
561                }
562            }
563
564            println!();
565            println!("Audit Summary: {} passed, {} warnings", passed, warnings);
566            if warnings == 0 {
567                println!("All skills passed basic security checks.");
568            } else {
569                println!("Review warnings above before using these skills.");
570            }
571        }
572        SkillsCommand::Publish { .. } => {
573            info!("publishing skill");
574            println!("Skills Publish");
575            println!("==============");
576            println!();
577            println!("Publishing skills to a registry:");
578            println!();
579            println!("1. Create a skill directory with SKILL.md:");
580            println!("   my-skill/SKILL.md");
581            println!();
582            println!("2. SKILL.md format:");
583            println!("   ---");
584            println!("   name: my-skill");
585            println!("   description: My awesome skill");
586            println!("   version: 1.0.0");
587            println!("   platforms: [windows, macos, linux]");
588            println!("   tags: [ai, automation]");
589            println!("   ---");
590            println!();
591            println!("3. Publish to registry (not yet implemented):");
592            println!("   hermes skills publish ./my-skill");
593            println!();
594            println!("Currently, skills are installed manually to:");
595            println!("  ~/.hermes/skills/<skill-name>/");
596        }
597        SkillsCommand::Snapshot(snapshot_cmd) => {
598            info!("skill snapshot command");
599            println!("Skills Snapshot");
600            println!("================");
601            println!();
602
603            match snapshot_cmd {
604                crate::SkillsSnapshotCommand::Export { output } => {
605                    println!("Exporting skills snapshot to: {}", output);
606                    println!();
607                    println!("Skill snapshot export feature is not yet fully implemented.");
608                    println!("Skills are stored in: ~/.hermes/skills/");
609                }
610                crate::SkillsSnapshotCommand::Import { input, force: _ } => {
611                    println!("Importing skills snapshot from: {}", input);
612                    println!();
613                    println!("Skill snapshot import feature is not yet fully implemented.");
614                }
615            }
616        }
617        SkillsCommand::Tap(tap_cmd) => {
618            println!("Skills Tap");
619            println!("==========");
620            println!();
621
622            match tap_cmd {
623                crate::SkillsTapCommand::Add { repo } => {
624                    println!("Adding skill tap from repo: {}", repo);
625                    println!();
626                    println!("Tap feature allows adding custom skill repositories.");
627                    println!("This is not yet implemented.");
628                    println!();
629                    println!("Workaround: Manually clone skill repos to:");
630                    println!("  ~/.hermes/skills/<skill-name>/");
631                }
632                crate::SkillsTapCommand::Remove { name } => {
633                    println!("Removing skill tap: {}", name);
634                    println!();
635                    println!("Tap feature allows adding custom skill repositories.");
636                    println!("This is not yet implemented.");
637                    println!();
638                    println!("To remove a skill manually:");
639                    println!("  hermes skills uninstall {}", name);
640                }
641                crate::SkillsTapCommand::List => {
642                    println!("Listing configured skill taps...");
643                    println!();
644
645                    let taps_file = SkillsIndex::skills_home().join(".hub").join("taps.yaml");
646                    if taps_file.exists() {
647                        match std::fs::read_to_string(&taps_file) {
648                            Ok(content) => {
649                                println!("Taps:");
650                                println!("{}", content);
651                            }
652                            Err(e) => {
653                                println!("Error reading taps file: {}", e);
654                            }
655                        }
656                    } else {
657                        println!("No skill taps configured.");
658                        println!();
659                        println!("To add a tap, you would run:");
660                        println!("  hermes skills tap add <git-url>");
661                    }
662                }
663            }
664        }
665        SkillsCommand::Config => {
666            info!("showing skills configuration");
667            println!("Skills Configuration");
668            println!("=====================");
669            println!();
670
671            let skills_home = SkillsIndex::skills_home();
672            println!("Skills directory: {:?}", skills_home);
673            println!();
674
675            let hub_dir = skills_home.join(".hub");
676            let index_path = hub_dir.join("index.yaml");
677            let taps_path = hub_dir.join("taps.yaml");
678
679            println!("Hub directory: {:?}", hub_dir);
680            println!("  Index: {}", if index_path.exists() { "exists" } else { "not found" });
681            println!("  Taps:  {}", if taps_path.exists() { "exists" } else { "not found" });
682            println!();
683
684            // Show environment variables affecting skills
685            println!("Environment:");
686            if std::env::var("HERMES_SKILLS_URL").is_ok() {
687                println!("  HERMES_SKILLS_URL: set");
688            } else {
689                println!("  HERMES_SKILLS_URL: not set (using default)");
690            }
691        }
692    }
693    Ok(())
694}
695
696pub async fn handle_gateway(cmd: GatewayCommand) -> Result<()> {
697    match cmd {
698        GatewayCommand::Run { platform, .. } => {
699            info!("running gateway: {:?}", platform);
700            if gateway_mod::is_gateway_running() {
701                println!("Gateway is already running.");
702                println!("Stop it first with: hermes gateway stop");
703                return Ok(());
704            }
705
706            println!("Starting Hermes Gateway...");
707            println!();
708            println!("NOTE: Full gateway implementation requires the agent runtime.");
709            println!("For now, this starts a minimal gateway process.");
710            println!();
711            println!("To run the full gateway:");
712            println!("  1. Ensure hermes-agent Python package is installed");
713            println!("  2. Run: python -m hermes_cli.main gateway run");
714            println!();
715
716            // Write PID file to indicate gateway "started"
717            if let Err(e) = gateway_mod::write_pid_file() {
718                eprintln!("Warning: Could not write PID file: {}", e);
719            }
720
721            // Write initial state
722            let state = gateway_mod::GatewayState {
723                gateway_state: "running".to_string(),
724                pid: std::process::id(),
725                platform: platform.clone(),
726                platform_state: Some("started".to_string()),
727                restart_requested: false,
728                active_agents: 0,
729                updated_at: chrono::Utc::now().to_rfc3339(),
730            };
731            let _ = gateway_mod::write_gateway_state(&state);
732
733            println!("Gateway started (PID: {})", std::process::id());
734            println!("View status with: hermes gateway status");
735        }
736        GatewayCommand::Start { .. } => {
737            info!("starting gateway service");
738            if gateway_mod::is_gateway_running() {
739                println!("Gateway is already running.");
740                return Ok(());
741            }
742
743            // Try Windows service first
744            if gateway_mod::is_service_installed() {
745                println!("Starting Hermes Gateway service...");
746                match gateway_mod::start_service() {
747                    Ok(()) => {
748                        println!("Gateway service started.");
749                        return Ok(());
750                    }
751                    Err(e) => {
752                        eprintln!("Warning: Could not start Windows service: {}", e);
753                        println!("Falling back to process mode...");
754                    }
755                }
756            }
757
758            // Fallback: start as process
759            println!("Starting Hermes Gateway...");
760            println!();
761            println!("On Windows, you can also install as a service:");
762            println!("  hermes gateway install");
763            println!();
764
765            if let Err(e) = gateway_mod::write_pid_file() {
766                eprintln!("Warning: Could not write PID file: {}", e);
767            }
768            println!("Gateway started.");
769        }
770        GatewayCommand::Stop { .. } => {
771            info!("stopping gateway service");
772
773            // Try Windows service first
774            let service_status = gateway_mod::get_service_status();
775            if service_status == gateway_mod::ServiceStatus::Running
776                || service_status == gateway_mod::ServiceStatus::StartPending
777            {
778                println!("Stopping Hermes Gateway service...");
779                match gateway_mod::stop_service() {
780                    Ok(()) => {
781                        println!("Gateway service stopped.");
782                        return Ok(());
783                    }
784                    Err(e) => {
785                        eprintln!("Warning: Could not stop Windows service: {}", e);
786                        println!("Falling back to process mode...");
787                    }
788                }
789            }
790
791            if !gateway_mod::is_gateway_running() {
792                println!("Gateway is not running.");
793                return Ok(());
794            }
795
796            println!("Stopping Hermes Gateway...");
797
798            // Write stopped state
799            let state = gateway_mod::GatewayState {
800                gateway_state: "stopped".to_string(),
801                pid: 0,
802                platform: None,
803                platform_state: Some("stopped".to_string()),
804                restart_requested: false,
805                active_agents: 0,
806                updated_at: chrono::Utc::now().to_rfc3339(),
807            };
808            let _ = gateway_mod::write_gateway_state(&state);
809
810            if let Err(e) = gateway_mod::remove_pid_file() {
811                eprintln!("Warning: Could not remove PID file: {}", e);
812            }
813
814            println!("Gateway stopped.");
815        }
816        GatewayCommand::Status { .. } => {
817            info!("checking gateway status");
818            println!("Hermes Gateway Status");
819            println!("====================");
820            println!();
821
822            // Show Windows service status
823            let service_status = gateway_mod::get_service_status();
824            if service_status != gateway_mod::ServiceStatus::NotApplicable {
825                println!("Service:  {}", service_status);
826                if service_status == gateway_mod::ServiceStatus::NotFound {
827                    println!("  (not installed as Windows service)");
828                }
829                println!();
830            }
831
832            if let Some(pid) = gateway_mod::get_running_pid() {
833                println!("Status:   RUNNING");
834                println!("PID:      {}", pid);
835                println!();
836
837                if let Some(state) = gateway_mod::read_gateway_state() {
838                    println!("Platform: {:?}", state.platform.unwrap_or_else(|| "N/A".to_string()));
839                    println!("State:     {}", state.gateway_state);
840                    println!("Agents:    {}", state.active_agents);
841                    if state.restart_requested {
842                        println!("Restart:   requested");
843                    }
844                }
845            } else {
846                println!("Status:   STOPPED");
847                println!();
848                if gateway_mod::is_service_installed() {
849                    println!("Start the service with: hermes gateway start");
850                    println!("Run interactively with:  hermes gateway run");
851                } else {
852                    println!("Start the gateway with: hermes gateway run");
853                    println!("Install as service:    hermes gateway install");
854                }
855            }
856        }
857        GatewayCommand::Setup { platform } => {
858            info!("setting up gateway: {:?}", platform);
859            println!("Gateway Setup");
860            println!("=============");
861            println!();
862
863            if let Some(p) = platform {
864                println!("Setting up platform: {}", p);
865            } else {
866                println!("Available platforms:");
867                println!("  telegram  - Telegram bot");
868                println!("  discord   - Discord bot");
869                println!("  slack     - Slack bot");
870                println!("  whatsapp  - WhatsApp integration");
871                println!();
872                println!("Run 'hermes gateway setup <platform>' to configure a specific platform.");
873            }
874
875            println!();
876            println!("Full gateway setup requires:");
877            println!("  1. hermes-agent Python package installed");
878            println!("  2. API keys configured via 'hermes auth add'");
879            println!("  3. Platform-specific setup via 'hermes gateway setup <platform>'");
880        }
881        GatewayCommand::Restart { system: _ } => {
882            info!("restarting gateway");
883            println!("Restarting Hermes Gateway...");
884
885            // Stop if running
886            let service_status = gateway_mod::get_service_status();
887            if service_status == gateway_mod::ServiceStatus::Running
888                || service_status == gateway_mod::ServiceStatus::StartPending
889            {
890                if let Err(e) = gateway_mod::stop_service() {
891                    eprintln!("Warning: Could not stop service: {}", e);
892                }
893            }
894
895            if gateway_mod::is_gateway_running() {
896                let state = gateway_mod::GatewayState {
897                    gateway_state: "stopped".to_string(),
898                    pid: 0,
899                    platform: None,
900                    platform_state: Some("restarting".to_string()),
901                    restart_requested: false,
902                    active_agents: 0,
903                    updated_at: chrono::Utc::now().to_rfc3339(),
904                };
905                let _ = gateway_mod::write_gateway_state(&state);
906                let _ = gateway_mod::remove_pid_file();
907            }
908
909            println!("Gateway stopped. Starting...");
910
911            // Start again
912            if gateway_mod::is_service_installed() {
913                println!("Starting Hermes Gateway service...");
914                match gateway_mod::start_service() {
915                    Ok(()) => {
916                        println!("Gateway service restarted.");
917                        return Ok(());
918                    }
919                    Err(e) => {
920                        eprintln!("Warning: Could not start Windows service: {}", e);
921                        println!("Falling back to process mode...");
922                    }
923                }
924            }
925
926            println!("Starting Hermes Gateway...");
927            if let Err(e) = gateway_mod::write_pid_file() {
928                eprintln!("Warning: Could not write PID file: {}", e);
929            }
930            let state = gateway_mod::GatewayState {
931                gateway_state: "running".to_string(),
932                pid: std::process::id(),
933                platform: None,
934                platform_state: Some("restarted".to_string()),
935                restart_requested: false,
936                active_agents: 0,
937                updated_at: chrono::Utc::now().to_rfc3339(),
938            };
939            let _ = gateway_mod::write_gateway_state(&state);
940            println!("Gateway restarted (PID: {}).", std::process::id());
941        }
942        GatewayCommand::Install { .. } => {
943            info!("installing gateway as Windows service");
944            println!("Gateway Install");
945            println!("==============");
946            println!();
947
948            #[cfg(target_os = "windows")]
949            {
950                println!("Installing Hermes Gateway as a Windows service...");
951                println!();
952
953                match gateway_mod::install_service() {
954                    Ok(()) => {
955                        println!("Gateway service installed successfully!");
956                        println!();
957                        println!("To start the service:");
958                        println!("  hermes gateway start");
959                        println!("  or");
960                        println!("  sc start HermesGateway");
961                        println!();
962                        println!("To check status:");
963                        println!("  hermes gateway status");
964                    }
965                    Err(e) => {
966                        anyhow::bail!("Failed to install service: {}", e);
967                    }
968                }
969            }
970
971            #[cfg(not(target_os = "windows"))]
972            {
973                println!("Windows service installation is only available on Windows.");
974                println!();
975                println!("On other platforms, use:");
976                println!("  hermes gateway run          - Run gateway interactively");
977                println!("  nohup hermes gateway run & - Run gateway in background");
978            }
979        }
980        GatewayCommand::Uninstall { .. } => {
981            info!("uninstalling gateway Windows service");
982            println!("Gateway Uninstall");
983            println!("================");
984            println!();
985
986            #[cfg(target_os = "windows")]
987            {
988                if !gateway_mod::is_service_installed() {
989                    println!("Gateway is not installed as a Windows service.");
990                    println!("Nothing to uninstall.");
991                    return Ok(());
992                }
993
994                println!("Uninstalling Hermes Gateway from Windows services...");
995                println!();
996
997                // Clean up PID and state files
998                let _ = gateway_mod::remove_pid_file();
999                let state = gateway_mod::GatewayState {
1000                    gateway_state: "uninstalled".to_string(),
1001                    pid: 0,
1002                    platform: None,
1003                    platform_state: Some("uninstalled".to_string()),
1004                    restart_requested: false,
1005                    active_agents: 0,
1006                    updated_at: chrono::Utc::now().to_rfc3339(),
1007                };
1008                let _ = gateway_mod::write_gateway_state(&state);
1009
1010                match gateway_mod::uninstall_service() {
1011                    Ok(()) => {
1012                        println!("Gateway service uninstalled successfully!");
1013                        println!();
1014                        println!("Note: Your data in ~/.hermes/ has been preserved.");
1015                    }
1016                    Err(e) => {
1017                        anyhow::bail!("Failed to uninstall service: {}", e);
1018                    }
1019                }
1020            }
1021
1022            #[cfg(not(target_os = "windows"))]
1023            {
1024                println!("Windows service uninstallation is only available on Windows.");
1025                println!("To stop the gateway: hermes gateway stop");
1026            }
1027        }
1028    }
1029    Ok(())
1030}
1031
1032pub async fn handle_cron(cmd: CronCommand) -> Result<()> {
1033    match cmd {
1034        CronCommand::List { .. } => {
1035            info!("listing cron jobs");
1036            println!("Hermes Cron Jobs");
1037            println!("================");
1038            println!();
1039
1040            let jobs = cron_mod::list_jobs(true)?;
1041
1042            if jobs.is_empty() {
1043                println!("No cron jobs configured.");
1044                println!();
1045                println!("Create a job with:");
1046                println!("  hermes cron add <schedule> <prompt>");
1047                println!();
1048                println!("Example:");
1049                println!("  hermes cron add 'every 30m' 'Check system status'");
1050            } else {
1051                for job in &jobs {
1052                    let status = if job.enabled { "[active]" } else { "[paused]" };
1053                    println!("{} {}", job.id, status);
1054                    println!("  Name:     {}", job.name);
1055                    println!("  Schedule: {}", job.schedule_display);
1056                    if let Some(ref next) = job.next_run_at {
1057                        println!("  Next run: {}", next);
1058                    }
1059                    if let Some(ref last) = job.last_run_at {
1060                        let last_status = job.last_status.as_deref().unwrap_or("N/A");
1061                        println!("  Last run: {} ({})", last, last_status);
1062                    }
1063                    if !job.skills.is_empty() {
1064                        println!("  Skills:   {}", job.skills.join(", "));
1065                    }
1066                    println!();
1067                }
1068            }
1069
1070            if !gateway_mod::is_gateway_running() {
1071                println!("NOTE: Gateway is not running - jobs won't fire automatically.");
1072                println!("Start it with: hermes gateway run");
1073            }
1074        }
1075        CronCommand::Add { schedule, command, .. } => {
1076            info!("adding cron job: {} -> {:?}", schedule, command);
1077            let prompt = command.unwrap_or_else(|| schedule.clone());
1078            match cron_mod::create_job(prompt, schedule) {
1079                Ok(job) => {
1080                    println!("Cron job created successfully!");
1081                    println!("  ID:       {}", job.id);
1082                    println!("  Name:     {}", job.name);
1083                    println!("  Schedule: {}", job.schedule_display);
1084                    println!();
1085                    if !gateway_mod::is_gateway_running() {
1086                        println!("NOTE: Start the gateway for jobs to run automatically:");
1087                        println!("  hermes gateway run");
1088                    }
1089                }
1090                Err(e) => {
1091                    anyhow::bail!("Failed to create cron job: {}", e);
1092                }
1093            }
1094        }
1095        CronCommand::Remove { id } => {
1096            info!("removing cron job: {}", id);
1097
1098            match cron_mod::remove_job(&id) {
1099                Ok(true) => {
1100                    println!("Cron job {} removed.", id);
1101                }
1102                Ok(false) => {
1103                    println!("Cron job '{}' not found.", id);
1104                }
1105                Err(e) => {
1106                    anyhow::bail!("Failed to remove cron job: {}", e);
1107                }
1108            }
1109        }
1110        CronCommand::Pause { id } => {
1111            info!("pausing cron job: {}", id);
1112
1113            match cron_mod::pause_job(&id, None) {
1114                Ok(Some(job)) => {
1115                    println!("Cron job '{}' paused.", job.name);
1116                }
1117                Ok(None) => {
1118                    println!("Cron job '{}' not found.", id);
1119                }
1120                Err(e) => {
1121                    anyhow::bail!("Failed to pause cron job: {}", e);
1122                }
1123            }
1124        }
1125        CronCommand::Resume { id } => {
1126            info!("resuming cron job: {}", id);
1127
1128            match cron_mod::resume_job(&id) {
1129                Ok(Some(job)) => {
1130                    println!("Cron job '{}' resumed.", job.name);
1131                    if let Some(ref next) = job.next_run_at {
1132                        println!("  Next run: {}", next);
1133                    }
1134                }
1135                Ok(None) => {
1136                    println!("Cron job '{}' not found.", id);
1137                }
1138                Err(e) => {
1139                    anyhow::bail!("Failed to resume cron job: {}", e);
1140                }
1141            }
1142        }
1143        CronCommand::Status => {
1144            info!("checking cron status");
1145            println!("Hermes Cron Status");
1146            println!("==================");
1147            println!();
1148
1149            let jobs = cron_mod::list_jobs(true)?;
1150            let active: usize = jobs.iter().filter(|j| j.enabled).count();
1151
1152            println!(
1153                "Gateway:  {}",
1154                if gateway_mod::is_gateway_running() { "running" } else { "stopped" }
1155            );
1156            println!("Jobs:     {} total, {} active", jobs.len(), active);
1157            println!();
1158
1159            if !jobs.is_empty() {
1160                println!("Due jobs: {}", cron_mod::get_due_jobs().len());
1161            }
1162
1163            if !gateway_mod::is_gateway_running() {
1164                println!();
1165                println!("NOTE: Gateway is not running - jobs won't fire.");
1166                println!("Start it with: hermes gateway run");
1167            }
1168        }
1169        CronCommand::Edit {
1170            job_id,
1171            schedule,
1172            prompt: _,
1173            name,
1174            deliver,
1175            repeat: _,
1176            skill: _,
1177            add_skill,
1178            remove_skill,
1179            clear_skills,
1180            script,
1181        } => {
1182            info!("editing cron job: {}", job_id);
1183            println!("Hermes Cron Edit");
1184            println!("================");
1185            println!();
1186
1187            let jobs = cron_mod::list_jobs(true)?;
1188            let job = jobs.iter().find(|j| j.id == *job_id);
1189
1190            match job {
1191                Some(j) => {
1192                    println!("Editing job: {}", j.name);
1193                    println!("  Current schedule: {}", j.schedule_display);
1194                    if let Some(s) = schedule {
1195                        println!("  New schedule: {}", s);
1196                    }
1197                    if let Some(n) = name {
1198                        println!("  New name: {}", n);
1199                    }
1200                    println!();
1201                    println!("Note: Full cron job editing requires:");
1202                    println!("  1. Remove the existing job: hermes cron remove {}", job_id);
1203                    println!("  2. Create a new job with updated settings: hermes cron add <schedule> <prompt>");
1204                    println!();
1205                    println!("Alternative parameters that can be edited:");
1206                    if add_skill.is_some() {
1207                        println!("  --add-skill <skill>");
1208                    }
1209                    if remove_skill.is_some() {
1210                        println!("  --remove-skill <skill>");
1211                    }
1212                    if clear_skills {
1213                        println!("  --clear-skills");
1214                    }
1215                    if deliver.is_some() {
1216                        println!("  --deliver <channel>");
1217                    }
1218                    if script.is_some() {
1219                        println!("  --script <script>");
1220                    }
1221                }
1222                None => {
1223                    println!("Job '{}' not found.", job_id);
1224                }
1225            }
1226        }
1227        CronCommand::Run { id } => {
1228            info!("running cron job manually: {}", id);
1229            println!("Hermes Cron Run");
1230            println!("================");
1231            println!();
1232
1233            let jobs = cron_mod::list_jobs(true)?;
1234            let job = jobs.iter().find(|j| j.id == *id);
1235
1236            match job {
1237                Some(j) => {
1238                    println!("Running cron job: {}", j.name);
1239                    println!("  Schedule: {}", j.schedule_display);
1240                    println!();
1241                    println!("Executing job now (dry-run - actual execution not implemented)...");
1242                    println!(
1243                        "  In production, this would execute the cron job prompt immediately."
1244                    );
1245                }
1246                None => {
1247                    println!("Job '{}' not found.", id);
1248                }
1249            }
1250        }
1251        CronCommand::Tick => {
1252            info!("cron tick - checking due jobs");
1253            let jobs = cron_mod::list_jobs(true)?;
1254            let due = cron_mod::get_due_jobs();
1255
1256            println!("Cron Tick");
1257            println!("=========");
1258            println!("Total jobs: {}", jobs.len());
1259            println!("Due now: {}", due.len());
1260
1261            if due.is_empty() {
1262                println!("No jobs are due for execution.");
1263            } else {
1264                println!("\nDue jobs:");
1265                for job in &due {
1266                    println!("  - {} ({})", job.name, job.id);
1267                }
1268            }
1269        }
1270    }
1271    Ok(())
1272}
1273
1274pub fn handle_config(cmd: ConfigCommand) -> Result<()> {
1275    match cmd {
1276        ConfigCommand::Show => {
1277            info!("showing configuration");
1278            let config = Config::load()?;
1279            println!("Hermes Configuration:");
1280            println!("  Config path: {:?}", Config::config_path());
1281            println!();
1282            println!("Model:");
1283            println!("  default: {}", config.model.default);
1284            println!("  provider: {}", config.model.provider);
1285            println!("  base_url: {}", config.model.base_url);
1286            println!();
1287            println!("Terminal:");
1288            println!("  env_type: {}", config.terminal.env_type);
1289            println!("  cwd: {}", config.terminal.cwd);
1290            println!("  timeout: {}", config.terminal.timeout);
1291            println!();
1292            println!("Display:");
1293            println!("  compact: {}", config.display.compact);
1294            println!("  resume_display: {}", config.display.resume_display);
1295            println!("  show_reasoning: {}", config.display.show_reasoning);
1296            println!("  streaming: {}", config.display.streaming);
1297            println!("  skin: {}", config.display.skin);
1298            println!();
1299            println!("Agent:");
1300            println!("  max_turns: {}", config.agent.max_turns);
1301            println!("  verbose: {}", config.agent.verbose);
1302            println!("  system_prompt: {}", config.agent.system_prompt);
1303            println!("  reasoning_effort: {}", config.agent.reasoning_effort);
1304        }
1305        ConfigCommand::Get { key } => {
1306            info!("getting config value: {}", key);
1307            let config = Config::load()?;
1308            let value = get_config_value(&config, &key)?;
1309            println!("{}", value);
1310        }
1311        ConfigCommand::Set { key, value } => {
1312            info!("setting config value: {} = {}", key, value);
1313            let mut config = Config::load()?;
1314            set_config_value(&mut config, &key, &value)?;
1315            config.save()?;
1316            println!("Set {} = {}", key, value);
1317        }
1318        ConfigCommand::Reset => {
1319            info!("resetting configuration to defaults");
1320            let config = Config::default();
1321            config.save()?;
1322            println!("Config reset to defaults");
1323        }
1324        ConfigCommand::Edit => {
1325            info!("editing configuration");
1326            let config_path = Config::config_path();
1327            println!("Hermes Config Edit");
1328            println!("=================");
1329            println!();
1330            println!("To edit your configuration, open the config file in your editor:");
1331            println!();
1332            println!("  Config file: {:?}", config_path);
1333            println!();
1334
1335            #[cfg(target_os = "windows")]
1336            {
1337                std::process::Command::new("cmd")
1338                    .args(["/C", "start", "", &config_path.to_string_lossy()])
1339                    .spawn()
1340                    .ok();
1341                println!("Opening in default editor...");
1342            }
1343
1344            #[cfg(target_os = "macos")]
1345            {
1346                std::process::Command::new("open").arg(&config_path).spawn().ok();
1347                println!("Opening in default editor...");
1348            }
1349
1350            #[cfg(target_os = "linux")]
1351            {
1352                if let Ok(editor) = std::env::var("EDITOR") {
1353                    std::process::Command::new(&editor).arg(&config_path).spawn().ok();
1354                    println!("Opening in ${}...", editor);
1355                } else {
1356                    println!("Set $EDITOR to open automatically, or open manually:");
1357                    println!("  nano {}", config_path.display());
1358                    println!("  vim {}", config_path.display());
1359                    println!("  code {}", config_path.display());
1360                }
1361            }
1362
1363            println!();
1364            println!("Alternatively, use these commands to set specific values:");
1365            println!("  hermes config set <key> <value>");
1366            println!();
1367            println!("Run 'hermes config show' to see current configuration.");
1368        }
1369        ConfigCommand::Path => {
1370            println!("{:?}", Config::config_path());
1371        }
1372        ConfigCommand::EnvPath => {
1373            let home = Config::hermes_home();
1374            println!("{:?}", home.join(".env"));
1375        }
1376        ConfigCommand::Check => {
1377            info!("checking configuration");
1378            println!("Config Check");
1379            println!("============");
1380            println!();
1381
1382            let config_path = Config::config_path();
1383            println!("Config file: {:?}", config_path);
1384            println!();
1385
1386            match Config::load() {
1387                Ok(config) => {
1388                    println!("[OK] Config file is valid YAML.");
1389                    println!();
1390                    println!("Current settings:");
1391                    println!("  Model: {}", config.model.default);
1392                    if !config.model.provider.is_empty() {
1393                        println!("  Provider: {}", config.model.provider);
1394                    }
1395                    println!("  Timeout: {}s", config.terminal.timeout);
1396                    println!("  Max turns: {}", config.agent.max_turns);
1397                }
1398                Err(e) => {
1399                    println!("[ERROR] Config file has issues: {}", e);
1400                    println!();
1401                    println!("Try 'hermes config reset' to restore defaults.");
1402                }
1403            }
1404        }
1405        ConfigCommand::Migrate => {
1406            info!("checking config migration");
1407            println!("Config Migrate");
1408            println!("=============");
1409            println!();
1410
1411            let config_path = Config::config_path();
1412            println!("Config file: {:?}", config_path);
1413            println!();
1414
1415            // Current version is 1 (no version field in config yet)
1416            const CURRENT_CONFIG_VERSION: u32 = 1;
1417            println!("Current config format version: {}", CURRENT_CONFIG_VERSION);
1418            println!();
1419
1420            if !config_path.exists() {
1421                println!("Config file does not exist yet.");
1422                println!("A new config will be created with default values.");
1423                return Ok(());
1424            }
1425
1426            // Try to load and re-save to validate format
1427            match Config::load() {
1428                Ok(_) => {
1429                    println!("[OK] Config file is valid and up-to-date.");
1430                    println!();
1431                    println!("Config is at the latest version ({}).", CURRENT_CONFIG_VERSION);
1432                    println!("No migration needed.");
1433                }
1434                Err(e) => {
1435                    println!("[WARN] Config file may be in an old format.");
1436                    println!("Error: {}", e);
1437                    println!();
1438                    println!("Migration instructions:");
1439                    println!("  1. Backup your config: cp config.yaml config.yaml.backup");
1440                    println!("  2. Try resetting: hermes config reset");
1441                    println!("  3. Or manually update the format to match the current schema");
1442                }
1443            }
1444        }
1445    }
1446    Ok(())
1447}
1448
1449fn get_config_value(config: &Config, key: &str) -> Result<String> {
1450    match key {
1451        "model.default" => Ok(config.model.default.clone()),
1452        "model.provider" => Ok(config.model.provider.clone()),
1453        "model.base_url" => Ok(config.model.base_url.clone()),
1454        "terminal.env_type" => Ok(config.terminal.env_type.clone()),
1455        "terminal.cwd" => Ok(config.terminal.cwd.clone()),
1456        "terminal.timeout" => Ok(config.terminal.timeout.to_string()),
1457        "display.compact" => Ok(config.display.compact.to_string()),
1458        "display.resume_display" => Ok(config.display.resume_display.clone()),
1459        "display.show_reasoning" => Ok(config.display.show_reasoning.to_string()),
1460        "display.streaming" => Ok(config.display.streaming.to_string()),
1461        "display.skin" => Ok(config.display.skin.clone()),
1462        "agent.max_turns" => Ok(config.agent.max_turns.to_string()),
1463        "agent.verbose" => Ok(config.agent.verbose.to_string()),
1464        "agent.system_prompt" => Ok(config.agent.system_prompt.clone()),
1465        "agent.reasoning_effort" => Ok(config.agent.reasoning_effort.clone()),
1466        _ => anyhow::bail!("Unknown config key: {}. Run 'hermes config show' for valid keys.", key),
1467    }
1468}
1469
1470fn set_config_value(config: &mut Config, key: &str, value: &str) -> Result<()> {
1471    match key {
1472        "model.default" => config.model.default = value.to_string(),
1473        "model.provider" => config.model.provider = value.to_string(),
1474        "model.base_url" => config.model.base_url = value.to_string(),
1475        "terminal.env_type" => config.terminal.env_type = value.to_string(),
1476        "terminal.cwd" => config.terminal.cwd = value.to_string(),
1477        "terminal.timeout" => {
1478            config.terminal.timeout = value.parse().map_err(|_| {
1479                anyhow::anyhow!("Invalid timeout value '{}': must be a positive integer", value)
1480            })?;
1481        }
1482        "display.compact" => {
1483            config.display.compact = value.parse().map_err(|_| {
1484                anyhow::anyhow!("Invalid compact value '{}': must be true or false", value)
1485            })?;
1486        }
1487        "display.resume_display" => config.display.resume_display = value.to_string(),
1488        "display.show_reasoning" => {
1489            config.display.show_reasoning = value.parse().map_err(|_| {
1490                anyhow::anyhow!("Invalid show_reasoning value '{}': must be true or false", value)
1491            })?;
1492        }
1493        "display.streaming" => {
1494            config.display.streaming = value.parse().map_err(|_| {
1495                anyhow::anyhow!("Invalid streaming value '{}': must be true or false", value)
1496            })?;
1497        }
1498        "display.skin" => config.display.skin = value.to_string(),
1499        "agent.max_turns" => {
1500            config.agent.max_turns = value.parse().map_err(|_| {
1501                anyhow::anyhow!("Invalid max_turns value '{}': must be a positive integer", value)
1502            })?;
1503        }
1504        "agent.verbose" => {
1505            config.agent.verbose = value.parse().map_err(|_| {
1506                anyhow::anyhow!("Invalid verbose value '{}': must be true or false", value)
1507            })?;
1508        }
1509        "agent.system_prompt" => config.agent.system_prompt = value.to_string(),
1510        "agent.reasoning_effort" => config.agent.reasoning_effort = value.to_string(),
1511        _ => {
1512            anyhow::bail!("Unknown config key: {}. Run 'hermes config show' for valid keys.", key);
1513        }
1514    }
1515    Ok(())
1516}
1517
1518pub fn handle_status() -> Result<()> {
1519    info!("showing status");
1520    let config = Config::load()?;
1521    let config_path = Config::config_path();
1522
1523    println!("Hermes CLI Status");
1524    println!("=================");
1525    println!("Version: {}", env!("CARGO_PKG_VERSION"));
1526    println!("Config: {:?}", config_path);
1527    if config_path.exists() {
1528        println!("Config file: exists");
1529    } else {
1530        println!("Config file: not found (using defaults)");
1531    }
1532    println!();
1533
1534    // Show effective model (session > global)
1535    let session_model = std::env::var("HERMES_SESSION_MODEL").ok();
1536    let effective_model = session_model.as_ref().unwrap_or(&config.model.default);
1537    println!("Model: {}", effective_model);
1538    if session_model.is_some() {
1539        println!("  (session override active)");
1540    }
1541    if !config.model.provider.is_empty() {
1542        println!("Provider: {}", config.model.provider);
1543    }
1544    println!();
1545    println!("Agent settings:");
1546    println!("  max_turns: {}", config.agent.max_turns);
1547    println!("  reasoning_effort: {}", config.agent.reasoning_effort);
1548    println!("  verbose: {}", config.agent.verbose);
1549
1550    Ok(())
1551}
1552
1553pub fn handle_setup(skip_auth: bool, skip_model: bool) -> Result<()> {
1554    info!("running setup wizard");
1555
1556    println!("Hermes CLI Setup");
1557    println!("================");
1558    println!();
1559
1560    // Check if Python hermes-agent is available
1561    println!("Checking hermes-agent installation...");
1562    let python_hermes = std::process::Command::new("python")
1563        .args(["-c", "import hermes_cli; print(hermes_cli.__file__)"])
1564        .output();
1565
1566    match python_hermes {
1567        Ok(output) if output.status.success() => {
1568            let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
1569            println!("  hermes-agent Python package: found at {}", path);
1570        }
1571        _ => {
1572            println!("  hermes-agent Python package: not found");
1573            println!();
1574            println!("  Install with:");
1575            println!("    pip install hermes-agent");
1576            println!();
1577        }
1578    }
1579
1580    if !skip_model {
1581        println!("\nModel Configuration:");
1582        println!("  Configure your AI provider with:");
1583        println!("    hermes auth add <provider> --api-key <key>");
1584        println!("    hermes model <model-name>");
1585        println!();
1586        println!("  Supported providers:");
1587        println!("    openai, anthropic, openrouter, gemini, etc.");
1588    }
1589
1590    if !skip_auth {
1591        println!("\nAuth Configuration:");
1592        let auth_store = AuthStore::load()?;
1593        if auth_store.credentials.is_empty() {
1594            println!("  No API keys configured.");
1595            println!("  Run 'hermes auth add <provider> --api-key <key>' to add credentials.");
1596        } else {
1597            println!("  Configured providers:");
1598            for cred in &auth_store.credentials {
1599                println!("    - {}", cred.provider);
1600            }
1601        }
1602    }
1603
1604    println!("\nGateway Setup:");
1605    println!("  Start the gateway with: hermes gateway run");
1606    println!("  Configure platforms with: hermes gateway setup <platform>");
1607
1608    println!("\nNext Steps:");
1609    println!("  1. Add your API key: hermes auth add <provider> --api-key <key>");
1610    println!("  2. Set your model: hermes model <model-name>");
1611    println!("  3. Start chatting: hermes chat");
1612
1613    println!();
1614    println!("For more help, see: https://hermes-agent.nousresearch.com/docs");
1615
1616    Ok(())
1617}
1618
1619#[allow(unused_variables)]
1620pub fn handle_doctor(_all: bool, _check: Option<&str>) -> Result<()> {
1621    info!("running doctor diagnostic");
1622
1623    println!("Hermes Doctor");
1624    println!("=============");
1625    println!();
1626
1627    let mut issues = 0;
1628    let mut warnings = 0;
1629
1630    // Check Python version
1631    println!("◆ Python");
1632    let python_version = std::process::Command::new("python").arg("--version").output();
1633
1634    match python_version {
1635        Ok(output) if output.status.success() => {
1636            let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
1637            println!("  ✓ Python installed: {}", version);
1638        }
1639        _ => {
1640            println!("  ✗ Python not found");
1641            issues += 1;
1642        }
1643    }
1644
1645    // Check hermes-agent Python package
1646    println!("\n◆ hermes-agent Package");
1647    let hermes_check = std::process::Command::new("python")
1648        .args(["-c", "import hermes_cli; print('ok')"])
1649        .output();
1650
1651    match hermes_check {
1652        Ok(output) if output.status.success() => {
1653            println!("  ✓ hermes-agent Python package installed");
1654        }
1655        _ => {
1656            println!("  ✗ hermes-agent Python package not installed");
1657            println!("    Install with: pip install hermes-agent");
1658            issues += 1;
1659        }
1660    }
1661
1662    // Check configuration
1663    println!("\n◆ Configuration");
1664    let config_path = Config::config_path();
1665    println!("  Config path: {:?}", config_path);
1666    if config_path.exists() {
1667        println!("  ✓ Config file exists");
1668    } else {
1669        println!("  ⚠ Config file not found (will use defaults)");
1670        warnings += 1;
1671    }
1672
1673    let config = Config::load()?;
1674    if config.model.default.is_empty() {
1675        println!("  ⚠ No default model configured");
1676        warnings += 1;
1677    } else {
1678        println!("  ✓ Default model: {}", config.model.default);
1679    }
1680
1681    // Check auth
1682    println!("\n◆ Authentication");
1683    let auth_store = AuthStore::load()?;
1684    if auth_store.credentials.is_empty() {
1685        println!("  ⚠ No API keys configured");
1686        warnings += 1;
1687    } else {
1688        println!("  ✓ API keys configured for {} provider(s)", auth_store.credentials.len());
1689    }
1690
1691    // Check gateway status
1692    println!("\n◆ Gateway");
1693    if gateway_mod::is_gateway_running() {
1694        println!("  ✓ Gateway is running");
1695    } else {
1696        println!("  ⚠ Gateway is not running");
1697        println!("    Start with: hermes gateway run");
1698        warnings += 1;
1699    }
1700
1701    // Check cron jobs
1702    println!("\n◆ Cron Jobs");
1703    let jobs = cron_mod::list_jobs(true).unwrap_or_default();
1704    let active: usize = jobs.iter().filter(|j| j.enabled).count();
1705    println!("  {} job(s) configured, {} active", jobs.len(), active);
1706
1707    // Summary
1708    println!("\n───────────────");
1709    if issues > 0 {
1710        println!("Result: {} issue(s) found", issues);
1711        println!("Fix the issues above for best experience.");
1712    } else if warnings > 0 {
1713        println!("Result: {} warning(s)", warnings);
1714        println!("Your setup is mostly working.");
1715    } else {
1716        println!("Result: All checks passed!");
1717        println!("Your Hermes CLI is properly configured.");
1718    }
1719
1720    Ok(())
1721}
1722
1723pub fn handle_update() -> Result<()> {
1724    info!("checking for updates");
1725
1726    println!("Hermes Update");
1727    println!("=============");
1728    println!();
1729
1730    println!("Checking for updates...");
1731    println!();
1732
1733    // For Rust CLI, we can't auto-update like Python
1734    // Just check git or show current version
1735    let version = env!("CARGO_PKG_VERSION");
1736    println!("Current version: {}", version);
1737    println!();
1738
1739    println!("To update Hermes CLI (Rust):");
1740    println!("  1. Download the latest release from:");
1741    println!("     https://github.com/nousresearch/hermes-agent/releases");
1742    println!();
1743    println!("  2. Or rebuild from source:");
1744    println!("     git pull origin main");
1745    println!("     cargo build --release");
1746    println!();
1747
1748    // Try to check if hermes-agent Python has updates
1749    println!("For hermes-agent Python package:");
1750    let pip_check =
1751        std::process::Command::new("pip").args(["index", "versions", "hermes-agent"]).output();
1752
1753    if let Ok(output) = pip_check {
1754        let output_str = String::from_utf8_lossy(&output.stdout);
1755        if output_str.contains("Available versions:") {
1756            println!("  hermes-agent Python package update info:");
1757            // Just show that we checked
1758            println!("  Run 'pip install --upgrade hermes-agent' to update");
1759        }
1760    }
1761
1762    Ok(())
1763}
1764
1765pub fn handle_uninstall() -> Result<()> {
1766    info!("running uninstall");
1767
1768    println!("Hermes Uninstall");
1769    println!("================");
1770    println!();
1771
1772    println!("This will remove the Hermes CLI (Rust) from your system.");
1773    println!();
1774
1775    println!("What would you like to do?");
1776    println!();
1777    println!("  1. Keep data (~/.hermes/) - Removes CLI only");
1778    println!("  2. Full uninstall - Removes everything including data");
1779    println!("  3. Cancel");
1780    println!();
1781
1782    // For automated uninstall, we'll do option 1 (keep data) by default
1783    // A real interactive mode would ask
1784
1785    println!("Running uninstall (keeping data)...");
1786    println!();
1787
1788    // Stop gateway if running
1789    if gateway_mod::is_gateway_running() {
1790        println!("Stopping gateway...");
1791        let state = gateway_mod::GatewayState {
1792            gateway_state: "stopped".to_string(),
1793            pid: 0,
1794            platform: None,
1795            platform_state: Some("uninstalled".to_string()),
1796            restart_requested: false,
1797            active_agents: 0,
1798            updated_at: chrono::Utc::now().to_rfc3339(),
1799        };
1800        let _ = gateway_mod::write_gateway_state(&state);
1801        let _ = gateway_mod::remove_pid_file();
1802    }
1803
1804    // Note: On Windows, removing the binary would be done by the installer
1805    println!("Hermes CLI (Rust) has been uninstalled.");
1806    println!();
1807    println!("Your data in ~/.hermes/ has been preserved.");
1808    println!();
1809    println!("To reinstall, download the latest release from:");
1810    println!("  https://github.com/nousresearch/hermes-agent/releases");
1811
1812    Ok(())
1813}
1814
1815// ── Stub handlers for new commands ──────────────────────────────────────────
1816
1817pub fn handle_sessions(cmd: SessionsCommand) {
1818    use hermes_session_db::SessionStore;
1819
1820    let home = crate::config::Config::hermes_home();
1821    let db_path = home.join("sessions.db");
1822    let store = match SessionStore::new(&db_path) {
1823        Ok(s) => s,
1824        Err(e) => {
1825            eprintln!("Error opening session database: {}", e);
1826            return;
1827        }
1828    };
1829
1830    match cmd {
1831        SessionsCommand::List { source: _, limit } => {
1832            let sessions = match store.list_sessions(limit as usize) {
1833                Ok(s) => s,
1834                Err(e) => {
1835                    eprintln!("Error listing sessions: {}", e);
1836                    return;
1837                }
1838            };
1839            if sessions.is_empty() {
1840                println!("No sessions found.");
1841                return;
1842            }
1843            println!("ID                                     Source          Model                Updated");
1844            println!("{}", "-".repeat(90));
1845            for s in &sessions {
1846                let updated = s.updated_at.format("%Y-%m-%d %H:%M").to_string();
1847                println!("{:<38} {:<15} {:<20} {}", s.id.to_string(), s.source, s.model, updated);
1848            }
1849            println!("\n{} session(s) shown.", sessions.len());
1850        }
1851        SessionsCommand::Export { output, source: _, session_id } => {
1852            let sid = match session_id {
1853                Some(id) => match id.parse::<uuid::Uuid>() {
1854                    Ok(u) => u,
1855                    Err(_) => {
1856                        eprintln!("Invalid session ID: {}", id);
1857                        return;
1858                    }
1859                },
1860                None => {
1861                    eprintln!("Please specify --session-id to export.");
1862                    return;
1863                }
1864            };
1865            let messages = match store.get_messages(&sid) {
1866                Ok(m) => m,
1867                Err(e) => {
1868                    eprintln!("Error reading session: {}", e);
1869                    return;
1870                }
1871            };
1872            let json = serde_json::to_string_pretty(&messages).unwrap_or_default();
1873            match std::fs::write(&output, &json) {
1874                Ok(_) => println!("Exported {} messages to '{}'.", messages.len(), output),
1875                Err(e) => eprintln!("Error writing file: {}", e),
1876            }
1877        }
1878        SessionsCommand::Delete { session_id, yes } => {
1879            let sid = match session_id.parse::<uuid::Uuid>() {
1880                Ok(u) => u,
1881                Err(_) => {
1882                    eprintln!("Invalid session ID: {}", session_id);
1883                    return;
1884                }
1885            };
1886            if !yes {
1887                println!("Are you sure you want to delete session {}? Use -y to confirm.", sid);
1888                return;
1889            }
1890            match store.delete_session(&sid) {
1891                Ok(_) => println!("Session {} deleted.", sid),
1892                Err(e) => eprintln!("Error deleting session: {}", e),
1893            }
1894        }
1895        SessionsCommand::Prune { older_than, source: _, yes: _ } => {
1896            // List sessions and filter by age
1897            let sessions = match store.list_sessions(1000) {
1898                Ok(s) => s,
1899                Err(e) => {
1900                    eprintln!("Error listing sessions: {}", e);
1901                    return;
1902                }
1903            };
1904            let cutoff = chrono::Utc::now() - chrono::Duration::days(older_than as i64);
1905            let old_sessions: Vec<_> = sessions.iter().filter(|s| s.updated_at < cutoff).collect();
1906            if old_sessions.is_empty() {
1907                println!("No sessions older than {} days found.", older_than);
1908                return;
1909            }
1910            println!("Found {} session(s) older than {} days.", old_sessions.len(), older_than);
1911            for s in &old_sessions {
1912                println!("  {} (updated: {})", s.id, s.updated_at.format("%Y-%m-%d"));
1913            }
1914            println!("Run with -y to confirm deletion.");
1915        }
1916        SessionsCommand::Stats => {
1917            let sessions = match store.list_sessions(10000) {
1918                Ok(s) => s,
1919                Err(e) => {
1920                    eprintln!("Error listing sessions: {}", e);
1921                    return;
1922                }
1923            };
1924            let total_messages: usize = sessions
1925                .iter()
1926                .filter_map(|s| store.get_messages(&s.id).ok())
1927                .map(|m| m.len())
1928                .sum();
1929            println!("Session Statistics:");
1930            println!("  Total sessions: {}", sessions.len());
1931            println!("  Total messages: {}", total_messages);
1932            if !sessions.is_empty() {
1933                let latest = &sessions[0];
1934                println!("  Latest session: {} ({})", latest.id, latest.model);
1935            }
1936        }
1937        SessionsCommand::Rename { session_id, title } => {
1938            // Session rename not supported in current schema — would need title column
1939            let _ = (session_id, title);
1940            println!("Session rename not yet supported in current schema.");
1941        }
1942        SessionsCommand::Browse { source: _, limit } => {
1943            // Browse is same as list with message preview
1944            let sessions = match store.list_sessions(limit as usize) {
1945                Ok(s) => s,
1946                Err(e) => {
1947                    eprintln!("Error listing sessions: {}", e);
1948                    return;
1949                }
1950            };
1951            if sessions.is_empty() {
1952                println!("No sessions found.");
1953                return;
1954            }
1955            for s in &sessions {
1956                println!("══ {} ══", s.id);
1957                println!(
1958                    "  Model: {} | Source: {} | Updated: {}",
1959                    s.model,
1960                    s.source,
1961                    s.updated_at.format("%Y-%m-%d %H:%M")
1962                );
1963                if let Ok(msgs) = store.get_messages(&s.id) {
1964                    for msg in msgs.iter().take(3) {
1965                        let preview: String = msg.content.chars().take(80).collect();
1966                        println!("  [{:?}] {}", msg.role, preview);
1967                    }
1968                    if msgs.len() > 3 {
1969                        println!("  ... and {} more messages", msgs.len() - 3);
1970                    }
1971                }
1972                println!();
1973            }
1974        }
1975    }
1976}
1977
1978pub fn handle_profile(cmd: ProfileCommand) {
1979    use crate::profiles;
1980
1981    match cmd {
1982        ProfileCommand::List => {
1983            let profiles = match profiles::list_profiles() {
1984                Ok(p) => p,
1985                Err(e) => {
1986                    eprintln!("Error listing profiles: {}", e);
1987                    return;
1988                }
1989            };
1990            let active = profiles::get_active_profile();
1991            if profiles.is_empty() {
1992                println!("No profiles found.");
1993                println!("Create one with: hermes profile create <name>");
1994            } else {
1995                println!("Profiles:");
1996                for name in profiles {
1997                    if name == active {
1998                        println!("  {} (*)", name);
1999                    } else {
2000                        println!("  {}", name);
2001                    }
2002                }
2003            }
2004        }
2005        ProfileCommand::Use { profile_name } => match profiles::load_profile(&profile_name) {
2006            Ok(config) => {
2007                if let Err(e) = config.save() {
2008                    eprintln!("Error saving config: {}", e);
2009                    return;
2010                }
2011                std::env::set_var("HERMES_PROFILE", &profile_name);
2012                println!("Switched to profile '{}'", profile_name);
2013                println!("Active profile will be '{}' on next launch", profile_name);
2014            }
2015            Err(e) => {
2016                eprintln!("Error loading profile '{}': {}", profile_name, e);
2017            }
2018        },
2019        ProfileCommand::Create { profile_name, clone, clone_all: _, clone_from, no_alias: _ } => {
2020            let result = if let Some(src) = clone_from {
2021                profiles::clone_profile(&src, &profile_name)
2022            } else if clone {
2023                let current = profiles::get_active_profile();
2024                if profiles::profile_exists(&current) {
2025                    profiles::clone_profile(&current, &profile_name)
2026                } else {
2027                    Err(anyhow::anyhow!("Cannot clone from '{}': profile not found", current))
2028                }
2029            } else {
2030                let config = Config::default();
2031                profiles::save_profile(&profile_name, &config)
2032            };
2033
2034            match result {
2035                Ok(()) => println!("Created profile '{}'", profile_name),
2036                Err(e) => eprintln!("Error creating profile: {}", e),
2037            }
2038        }
2039        ProfileCommand::Delete { profile_name, yes } => {
2040            if !yes {
2041                eprintln!("This will delete profile '{}'. Use --yes to confirm.", profile_name);
2042                return;
2043            }
2044            match profiles::delete_profile(&profile_name) {
2045                Ok(()) => println!("Deleted profile '{}'", profile_name),
2046                Err(e) => eprintln!("Error deleting profile: {}", e),
2047            }
2048        }
2049        ProfileCommand::Show { profile_name } => match profiles::load_profile(&profile_name) {
2050            Ok(config) => {
2051                let yaml = serde_yaml::to_string(&config).unwrap_or_default();
2052                println!("Profile '{}':", profile_name);
2053                println!("{}", yaml);
2054            }
2055            Err(e) => eprintln!("Error loading profile: {}", e),
2056        },
2057        ProfileCommand::Alias { profile_name, remove, alias_name } => {
2058            if remove {
2059                if let Some(alias) = alias_name {
2060                    match profiles::delete_profile(&alias) {
2061                        Ok(()) => println!("Removed alias '{}'", alias),
2062                        Err(e) => eprintln!("Error removing alias: {}", e),
2063                    }
2064                } else {
2065                    eprintln!("Specify alias name with --alias-name <name>");
2066                }
2067            } else if let Some(alias) = alias_name {
2068                match profiles::clone_profile(&profile_name, &alias) {
2069                    Ok(()) => println!("Created alias '{}' -> '{}'", alias, profile_name),
2070                    Err(e) => eprintln!("Error creating alias: {}", e),
2071                }
2072            } else {
2073                eprintln!("Specify alias name with --alias-name <name>");
2074            }
2075        }
2076        ProfileCommand::Rename { old_name, new_name } => {
2077            match profiles::rename_profile(&old_name, &new_name) {
2078                Ok(()) => println!("Renamed profile '{}' to '{}'", old_name, new_name),
2079                Err(e) => eprintln!("Error renaming profile: {}", e),
2080            }
2081        }
2082        ProfileCommand::Export { profile_name, output } => {
2083            let config = match profiles::load_profile(&profile_name) {
2084                Ok(c) => c,
2085                Err(e) => {
2086                    eprintln!("Error loading profile: {}", e);
2087                    return;
2088                }
2089            };
2090            let output_path = output
2091                .map(PathBuf::from)
2092                .unwrap_or_else(|| PathBuf::from(format!("{}.yaml", profile_name)));
2093            let yaml = match serde_yaml::to_string(&config) {
2094                Ok(y) => y,
2095                Err(e) => {
2096                    eprintln!("Error serializing profile: {}", e);
2097                    return;
2098                }
2099            };
2100            if let Err(e) = fs::write(&output_path, yaml) {
2101                eprintln!("Error writing export file: {}", e);
2102                return;
2103            }
2104            println!("Exported profile '{}' to {:?}", profile_name, output_path);
2105        }
2106        ProfileCommand::Import { archive, import_name } => {
2107            let content = match fs::read_to_string(&archive) {
2108                Ok(c) => c,
2109                Err(e) => {
2110                    eprintln!("Error reading import file: {}", e);
2111                    return;
2112                }
2113            };
2114            let config: Config = match serde_yaml::from_str(&content) {
2115                Ok(c) => c,
2116                Err(e) => {
2117                    eprintln!("Error parsing YAML: {}", e);
2118                    return;
2119                }
2120            };
2121            let name = import_name.unwrap_or_else(|| "imported".to_string());
2122            match profiles::save_profile(&name, &config) {
2123                Ok(()) => println!("Imported profile as '{}'", name),
2124                Err(e) => eprintln!("Error saving profile: {}", e),
2125            }
2126        }
2127    }
2128}
2129
2130pub fn handle_mcp(cmd: McpCommand) {
2131    use crate::mcp;
2132
2133    match cmd {
2134        McpCommand::Serve { verbose: _ } => {
2135            println!("Hermes MCP Serve Mode");
2136            println!();
2137            println!("MCP (Model Context Protocol) servers can be configured to extend Hermes");
2138            println!("with additional tools and capabilities.");
2139            println!();
2140            println!("Configuration file: ~/.hermes/mcp.json");
2141            println!();
2142            println!("To add an MCP server:");
2143            println!("  hermes mcp add <name> --url <url>");
2144            println!();
2145            println!("Example MCP servers:");
2146            println!("  hermes mcp add filesystem --url stdio://npx -y @modelcontextprotocol/server-filesystem /path/to/dir");
2147            println!(
2148                "  hermes mcp add memory --url stdio://npx -y @modelcontextprotocol/server-memory"
2149            );
2150        }
2151        McpCommand::Add { name, url, command: _, args: _, auth: _, preset: _, env: _ } => {
2152            let url = match url {
2153                Some(u) => u,
2154                None => {
2155                    eprintln!("Error: --url is required");
2156                    return;
2157                }
2158            };
2159            let mut store = match mcp::McpStore::load() {
2160                Ok(s) => s,
2161                Err(e) => {
2162                    eprintln!("Error loading MCP store: {}", e);
2163                    return;
2164                }
2165            };
2166            if let Err(e) = store.add_server(&name, &url) {
2167                eprintln!("Error adding server: {}", e);
2168                return;
2169            }
2170            if let Err(e) = store.save() {
2171                eprintln!("Error saving MCP store: {}", e);
2172                return;
2173            }
2174            println!("Added MCP server '{}' with URL {}", name, url);
2175        }
2176        McpCommand::Remove { name } => {
2177            let mut store = match mcp::McpStore::load() {
2178                Ok(s) => s,
2179                Err(e) => {
2180                    eprintln!("Error loading MCP store: {}", e);
2181                    return;
2182                }
2183            };
2184            if let Err(e) = store.remove_server(&name) {
2185                eprintln!("Error removing server: {}", e);
2186                return;
2187            }
2188            if let Err(e) = store.save() {
2189                eprintln!("Error saving MCP store: {}", e);
2190                return;
2191            }
2192            println!("Removed MCP server '{}'", name);
2193        }
2194        McpCommand::List => {
2195            let store = match mcp::McpStore::load() {
2196                Ok(s) => s,
2197                Err(e) => {
2198                    eprintln!("Error loading MCP store: {}", e);
2199                    return;
2200                }
2201            };
2202            let servers = store.list_servers();
2203            if servers.is_empty() {
2204                println!("No MCP servers configured.");
2205                println!("Add one with: hermes mcp add <name> --url <url>");
2206            } else {
2207                println!("MCP Servers:");
2208                println!("{:<20} {:<40} Enabled", "Name", "URL");
2209                println!("{}", "-".repeat(80));
2210                for server in servers {
2211                    println!("{:<20} {:<40} {}", server.name, server.url, server.enabled);
2212                }
2213            }
2214        }
2215        McpCommand::Test { name } => {
2216            let store = match mcp::McpStore::load() {
2217                Ok(s) => s,
2218                Err(e) => {
2219                    eprintln!("Error loading MCP store: {}", e);
2220                    return;
2221                }
2222            };
2223            let server = match store.get_server(&name) {
2224                Some(s) => s,
2225                None => {
2226                    eprintln!("MCP server '{}' not found", name);
2227                    return;
2228                }
2229            };
2230            print!("Testing connection to '{}'... ", name);
2231            match mcp::test_server(server) {
2232                Ok(result) => {
2233                    if result.success {
2234                        println!("OK");
2235                        println!("  Response time: {}ms", result.response_time_ms);
2236                        println!("  {}", result.message);
2237                    } else {
2238                        println!("FAILED");
2239                        println!("  {}", result.message);
2240                    }
2241                }
2242                Err(e) => {
2243                    println!("ERROR");
2244                    eprintln!("  {}", e);
2245                }
2246            }
2247        }
2248        McpCommand::Configure { name: _ } => {
2249            let path = mcp::McpStore::mcp_path();
2250            println!("MCP configuration file: {:?}", path);
2251            println!();
2252            println!("To edit the MCP configuration, open this file in your editor:");
2253            println!("  {:?}", path);
2254            println!();
2255            println!("File format:");
2256            println!("{{");
2257            println!("  \"servers\": [");
2258            println!("    {{");
2259            println!("      \"name\": \"example\",");
2260            println!("      \"url\": \"stdio://npx -y @modelcontextprotocol/server-example\",");
2261            println!("      \"enabled\": true");
2262            println!("    }}");
2263            println!("  ]");
2264            println!("}}");
2265        }
2266    }
2267}
2268
2269pub fn handle_memory(cmd: MemoryCommand) -> Result<()> {
2270    match cmd {
2271        MemoryCommand::Setup => handle_memory_setup(),
2272        MemoryCommand::Status => handle_memory_status(),
2273        MemoryCommand::Off => handle_memory_off(),
2274    }
2275}
2276
2277fn get_memory_dir() -> PathBuf {
2278    Config::hermes_home().join("memory")
2279}
2280
2281fn get_memory_file(name: &str) -> PathBuf {
2282    get_memory_dir().join(format!("{}.json", name))
2283}
2284
2285fn ensure_memory_dir() -> Result<PathBuf> {
2286    let dir = get_memory_dir();
2287    if !dir.exists() {
2288        fs::create_dir_all(&dir)
2289            .with_context(|| format!("failed to create memory directory at {:?}", dir))?;
2290    }
2291    Ok(dir)
2292}
2293
2294fn read_memory_json(name: &str) -> Result<serde_json::Value> {
2295    let path = get_memory_file(name);
2296    if !path.exists() {
2297        return Ok(serde_json::json!({ "entries": [] }));
2298    }
2299    let content = fs::read_to_string(&path)
2300        .with_context(|| format!("failed to read memory file {:?}", path))?;
2301    serde_json::from_str(&content)
2302        .with_context(|| format!("failed to parse memory file {:?}", path))
2303}
2304
2305fn write_memory_json(name: &str, value: &serde_json::Value) -> Result<()> {
2306    let path = get_memory_file(name);
2307    let content = serde_json::to_string_pretty(value).context("failed to serialize memory data")?;
2308    fs::write(&path, content).with_context(|| format!("failed to write memory file {:?}", path))?;
2309    Ok(())
2310}
2311
2312fn handle_memory_setup() -> Result<()> {
2313    let dir = ensure_memory_dir()?;
2314    println!("Created memory directory: {:?}", dir);
2315
2316    // Create each memory file with empty entries
2317    let files = ["preferences", "facts", "context", "settings"];
2318    for name in files {
2319        let path = get_memory_file(name);
2320        if path.exists() {
2321            println!("  {}: already exists", name);
2322        } else {
2323            let default_value = if name == "settings" {
2324                serde_json::json!({ "enabled": true })
2325            } else {
2326                serde_json::json!({ "entries": [] })
2327            };
2328            write_memory_json(name, &default_value)?;
2329            println!("  {}: created", name);
2330        }
2331    }
2332
2333    println!("\nMemory setup complete. Memory is enabled.");
2334    println!("Run 'hermes memory off' to disable memory storage.");
2335    Ok(())
2336}
2337
2338fn handle_memory_status() -> Result<()> {
2339    let dir = get_memory_dir();
2340
2341    if !dir.exists() {
2342        println!("Memory is not initialized.");
2343        println!("Run 'hermes memory setup' to initialize memory storage.");
2344        return Ok(());
2345    }
2346
2347    // Read settings to check if memory is enabled
2348    let settings = read_memory_json("settings")?;
2349    let enabled = settings.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true);
2350
2351    // Count files and calculate total size
2352    let mut total_size: u64 = 0;
2353    let mut file_count = 0;
2354    let mut file_info: Vec<(String, usize, u64)> = Vec::new();
2355
2356    let files = ["preferences", "facts", "context", "settings"];
2357    for name in files {
2358        let path = get_memory_file(name);
2359        if path.exists() {
2360            let metadata = fs::metadata(&path)?;
2361            let size = metadata.len();
2362            total_size += size;
2363            file_count += 1;
2364
2365            // Count entries
2366            let entries = if let Ok(content) = fs::read_to_string(&path) {
2367                if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
2368                    json.get("entries").and_then(|e| e.as_array()).map(|arr| arr.len()).unwrap_or(0)
2369                } else {
2370                    0
2371                }
2372            } else {
2373                0
2374            };
2375
2376            file_info.push((name.to_string(), entries, size));
2377        }
2378    }
2379
2380    println!("Memory Status:");
2381    println!("  Location: {:?}", dir);
2382    println!("  Status: {}", if enabled { "Enabled" } else { "Disabled" });
2383    println!("  Files: {} (total {} bytes)", file_count, total_size);
2384    println!("\nMemory Files:");
2385    for (name, entries, size) in file_info {
2386        println!("  {}: {} entries ({} bytes)", name, entries, size);
2387    }
2388
2389    Ok(())
2390}
2391
2392fn handle_memory_off() -> Result<()> {
2393    ensure_memory_dir()?;
2394
2395    let settings_path = get_memory_file("settings");
2396    let settings = if settings_path.exists() {
2397        read_memory_json("settings")?
2398    } else {
2399        serde_json::json!({ "enabled": true })
2400    };
2401
2402    let mut settings_obj = settings.as_object().cloned().unwrap_or_default();
2403    settings_obj.insert("enabled".to_string(), serde_json::json!(false));
2404
2405    write_memory_json("settings", &serde_json::Value::Object(settings_obj))?;
2406
2407    println!("Memory has been disabled.");
2408    println!("Your existing memory files are preserved.");
2409    println!("Run 'hermes memory setup' to re-enable memory storage.");
2410    Ok(())
2411}
2412
2413pub fn handle_webhook(cmd: WebhookCommand) {
2414    match cmd {
2415        WebhookCommand::Subscribe {
2416            name,
2417            prompt,
2418            events,
2419            description,
2420            skills,
2421            deliver,
2422            deliver_chat_id,
2423            secret,
2424        } => {
2425            info!("subscribing webhook: {}", name);
2426            let events: Vec<String> = if events.is_empty() {
2427                vec!["message".to_string()]
2428            } else {
2429                events.split(',').map(|s| s.trim().to_string()).collect()
2430            };
2431
2432            let webhook = Webhook {
2433                name: name.clone(),
2434                url: prompt, // Using prompt field as URL since that's the actual webhook URL
2435                events,
2436                enabled: true,
2437                description,
2438                skills: if skills.is_empty() {
2439                    vec![]
2440                } else {
2441                    skills.split(',').map(|s| s.trim().to_string()).collect()
2442                },
2443                deliver,
2444                deliver_chat_id,
2445                secret,
2446                added_at: chrono::Utc::now().to_rfc3339(),
2447            };
2448
2449            let mut store = match WebhookStore::load() {
2450                Ok(s) => s,
2451                Err(e) => {
2452                    eprintln!("Error loading webhook store: {}", e);
2453                    return;
2454                }
2455            };
2456
2457            if let Err(e) = store.add_webhook(webhook) {
2458                eprintln!("Error adding webhook: {}", e);
2459                return;
2460            }
2461
2462            if let Err(e) = store.save() {
2463                eprintln!("Error saving webhook store: {}", e);
2464                return;
2465            }
2466
2467            println!("Webhook '{}' subscribed successfully.", name);
2468        }
2469        WebhookCommand::List => {
2470            info!("listing webhooks");
2471            let store = match WebhookStore::load() {
2472                Ok(s) => s,
2473                Err(e) => {
2474                    eprintln!("Error loading webhook store: {}", e);
2475                    return;
2476                }
2477            };
2478
2479            let webhooks = store.list_webhooks();
2480            if webhooks.is_empty() {
2481                println!("No webhooks configured.");
2482                println!("Add one with: hermes webhook subscribe <name> --prompt <url>");
2483            } else {
2484                println!("Webhooks:");
2485                println!("{:<20} {:<40} {:<15} Enabled", "Name", "URL", "Events");
2486                println!("{}", "-".repeat(90));
2487                for webhook in webhooks {
2488                    let events = if webhook.events.is_empty() {
2489                        "none".to_string()
2490                    } else {
2491                        webhook.events.join(",")
2492                    };
2493                    println!(
2494                        "{:<20} {:<40} {:<15} {}",
2495                        webhook.name, webhook.url, events, webhook.enabled
2496                    );
2497                }
2498            }
2499        }
2500        WebhookCommand::Remove { name } => {
2501            info!("removing webhook: {}", name);
2502            let mut store = match WebhookStore::load() {
2503                Ok(s) => s,
2504                Err(e) => {
2505                    eprintln!("Error loading webhook store: {}", e);
2506                    return;
2507                }
2508            };
2509
2510            if let Err(e) = store.remove_webhook(&name) {
2511                eprintln!("Error removing webhook: {}", e);
2512                return;
2513            }
2514
2515            if let Err(e) = store.save() {
2516                eprintln!("Error saving webhook store: {}", e);
2517                return;
2518            }
2519
2520            println!("Webhook '{}' removed.", name);
2521        }
2522        WebhookCommand::Test { name, payload } => {
2523            info!("testing webhook: {}", name);
2524            let store = match WebhookStore::load() {
2525                Ok(s) => s,
2526                Err(e) => {
2527                    eprintln!("Error loading webhook store: {}", e);
2528                    return;
2529                }
2530            };
2531
2532            let webhook = match store.get_webhook(&name) {
2533                Some(w) => w,
2534                None => {
2535                    eprintln!("Webhook '{}' not found", name);
2536                    return;
2537                }
2538            };
2539
2540            // Validate URL format
2541            if !webhook.url.starts_with("http://") && !webhook.url.starts_with("https://") {
2542                eprintln!(
2543                    "Invalid webhook URL: {}. Must start with http:// or https://",
2544                    webhook.url
2545                );
2546                return;
2547            }
2548
2549            println!("Testing webhook '{}' at {}", name, webhook.url);
2550            println!(
2551                "Payload: {}",
2552                if payload.is_empty() { "(empty)".to_string() } else { payload.clone() }
2553            );
2554
2555            // For local/testing purposes, just validate URL format
2556            // Actual HTTP POST test would require reqwest or similar
2557            println!("URL format validated: OK");
2558            if !webhook.enabled {
2559                println!("WARNING: Webhook is disabled");
2560            }
2561            println!("Test complete. Configure your server to receive webhooks at the URL above.");
2562        }
2563    }
2564}
2565
2566pub fn handle_pairing(cmd: PairingCommand) {
2567    match cmd {
2568        PairingCommand::List => {
2569            info!("listing pairings");
2570            let store = match PairingStore::load() {
2571                Ok(s) => s,
2572                Err(e) => {
2573                    eprintln!("Error loading pairing store: {}", e);
2574                    return;
2575                }
2576            };
2577
2578            let pairings = store.list_pairings();
2579            if pairings.is_empty() {
2580                println!("No pairings configured.");
2581                println!("Pairings allow other platforms to connect to Hermes.");
2582                return;
2583            }
2584
2585            println!("Pairings:");
2586            println!("{:<15} {:<20} {:<15} Created", "Platform", "User ID", "Status");
2587            println!("{}", "-".repeat(80));
2588
2589            for pairing in pairings {
2590                let status = match pairing.status {
2591                    PairingStatus::Pending => "pending",
2592                    PairingStatus::Approved => "approved",
2593                    PairingStatus::Revoked => "revoked",
2594                };
2595                println!(
2596                    "{:<15} {:<20} {:<15} {}",
2597                    pairing.platform, pairing.user_id, status, pairing.created_at
2598                );
2599            }
2600
2601            // Show summary by status
2602            let pending = store.list_by_status(&PairingStatus::Pending).len();
2603            let approved = store.list_by_status(&PairingStatus::Approved).len();
2604            let revoked = store.list_by_status(&PairingStatus::Revoked).len();
2605            println!("\nSummary: {} pending, {} approved, {} revoked", pending, approved, revoked);
2606        }
2607        PairingCommand::Approve { platform, code } => {
2608            info!("approving pairing: platform={}, code={}", platform, code);
2609            let mut store = match PairingStore::load() {
2610                Ok(s) => s,
2611                Err(e) => {
2612                    eprintln!("Error loading pairing store: {}", e);
2613                    return;
2614                }
2615            };
2616
2617            if let Err(e) = store.approve_pairing(&platform, &code) {
2618                eprintln!("Error approving pairing: {}", e);
2619                return;
2620            }
2621
2622            if let Err(e) = store.save() {
2623                eprintln!("Error saving pairing store: {}", e);
2624                return;
2625            }
2626
2627            println!("Pairing approved for platform '{}'.", platform);
2628        }
2629        PairingCommand::Revoke { platform, user_id } => {
2630            info!("revoking pairing: platform={}, user_id={}", platform, user_id);
2631            let mut store = match PairingStore::load() {
2632                Ok(s) => s,
2633                Err(e) => {
2634                    eprintln!("Error loading pairing store: {}", e);
2635                    return;
2636                }
2637            };
2638
2639            if let Err(e) = store.revoke_pairing(&platform, &user_id) {
2640                eprintln!("Error revoking pairing: {}", e);
2641                return;
2642            }
2643
2644            if let Err(e) = store.save() {
2645                eprintln!("Error saving pairing store: {}", e);
2646                return;
2647            }
2648
2649            println!("Pairing revoked for platform '{}', user '{}'.", platform, user_id);
2650        }
2651        PairingCommand::ClearPending => {
2652            info!("clearing pending pairings");
2653            let mut store = match PairingStore::load() {
2654                Ok(s) => s,
2655                Err(e) => {
2656                    eprintln!("Error loading pairing store: {}", e);
2657                    return;
2658                }
2659            };
2660
2661            match store.clear_pending() {
2662                Ok(()) => {
2663                    if let Err(e) = store.save() {
2664                        eprintln!("Error saving pairing store: {}", e);
2665                        return;
2666                    }
2667                    println!("All pending pairings cleared.");
2668                }
2669                Err(e) => {
2670                    eprintln!("{}", e);
2671                }
2672            }
2673        }
2674    }
2675}
2676
2677pub fn handle_plugins(cmd: PluginsCommand) {
2678    match cmd {
2679        PluginsCommand::Install { identifier, force: _ } => {
2680            info!("installing plugin: {}", identifier);
2681            let mut store = match PluginStore::load() {
2682                Ok(s) => s,
2683                Err(e) => {
2684                    eprintln!("Error loading plugin store: {}", e);
2685                    return;
2686                }
2687            };
2688
2689            // Parse identifier as name and optional source
2690            let parts: Vec<&str> = identifier.split('@').collect();
2691            let name = parts[0].to_string();
2692            let source = if parts.len() > 1 { parts[1] } else { "local" }.to_string();
2693
2694            // Check if already installed
2695            if store.get_plugin(&name).is_some() {
2696                eprintln!(
2697                    "Plugin '{}' is already installed. Use 'hermes plugins update {}' to update.",
2698                    name, name
2699                );
2700                return;
2701            }
2702
2703            let plugin = Plugin {
2704                name: name.clone(),
2705                version: "1.0.0".to_string(), // Default version
2706                source,
2707                enabled: true,
2708                description: format!("Plugin: {}", name),
2709                author: "Unknown".to_string(),
2710                installed_at: chrono::Utc::now().to_rfc3339(),
2711                updated_at: chrono::Utc::now().to_rfc3339(),
2712            };
2713
2714            if let Err(e) = store.add_plugin(plugin) {
2715                eprintln!("Error installing plugin: {}", e);
2716                return;
2717            }
2718
2719            if let Err(e) = store.save() {
2720                eprintln!("Error saving plugin store: {}", e);
2721                return;
2722            }
2723
2724            println!("Plugin '{}' installed successfully.", name);
2725        }
2726        PluginsCommand::Update { name } => {
2727            info!("updating plugin: {}", name);
2728            let mut store = match PluginStore::load() {
2729                Ok(s) => s,
2730                Err(e) => {
2731                    eprintln!("Error loading plugin store: {}", e);
2732                    return;
2733                }
2734            };
2735
2736            // Bump version
2737            let new_version = "1.1.0".to_string(); // Simple bump for now
2738            if let Err(e) = store.update_plugin(&name, &new_version) {
2739                eprintln!("Error updating plugin: {}", e);
2740                return;
2741            }
2742
2743            if let Err(e) = store.save() {
2744                eprintln!("Error saving plugin store: {}", e);
2745                return;
2746            }
2747
2748            println!("Plugin '{}' updated to version {}.", name, new_version);
2749        }
2750        PluginsCommand::Remove { name } => {
2751            info!("removing plugin: {}", name);
2752            let mut store = match PluginStore::load() {
2753                Ok(s) => s,
2754                Err(e) => {
2755                    eprintln!("Error loading plugin store: {}", e);
2756                    return;
2757                }
2758            };
2759
2760            if let Err(e) = store.remove_plugin(&name) {
2761                eprintln!("Error removing plugin: {}", e);
2762                return;
2763            }
2764
2765            if let Err(e) = store.save() {
2766                eprintln!("Error saving plugin store: {}", e);
2767                return;
2768            }
2769
2770            println!("Plugin '{}' removed.", name);
2771        }
2772        PluginsCommand::List => {
2773            info!("listing plugins");
2774            let store = match PluginStore::load() {
2775                Ok(s) => s,
2776                Err(e) => {
2777                    eprintln!("Error loading plugin store: {}", e);
2778                    return;
2779                }
2780            };
2781
2782            let plugins = store.list_plugins();
2783            if plugins.is_empty() {
2784                println!("No plugins installed.");
2785                println!("Install one with: hermes plugins install <identifier>");
2786                return;
2787            }
2788
2789            println!("Plugins:");
2790            println!(
2791                "{:<20} {:<10} {:<15} {:<40} Description",
2792                "Name", "Version", "Enabled", "Source"
2793            );
2794            println!("{}", "-".repeat(100));
2795
2796            for plugin in plugins {
2797                println!(
2798                    "{:<20} {:<10} {:<15} {:<40} {}",
2799                    plugin.name,
2800                    plugin.version,
2801                    plugin.enabled,
2802                    plugin.source,
2803                    if plugin.description.len() > 40 {
2804                        format!("{}...", &plugin.description[..37])
2805                    } else {
2806                        plugin.description.clone()
2807                    }
2808                );
2809            }
2810
2811            let enabled = plugins.iter().filter(|p| p.enabled).count();
2812            println!("\n{} plugin(s) installed, {} enabled", plugins.len(), enabled);
2813        }
2814        PluginsCommand::Enable { name } => {
2815            info!("enabling plugin: {}", name);
2816            let mut store = match PluginStore::load() {
2817                Ok(s) => s,
2818                Err(e) => {
2819                    eprintln!("Error loading plugin store: {}", e);
2820                    return;
2821                }
2822            };
2823
2824            if let Err(e) = store.enable_plugin(&name) {
2825                eprintln!("Error enabling plugin: {}", e);
2826                return;
2827            }
2828
2829            if let Err(e) = store.save() {
2830                eprintln!("Error saving plugin store: {}", e);
2831                return;
2832            }
2833
2834            println!("Plugin '{}' enabled.", name);
2835        }
2836        PluginsCommand::Disable { name } => {
2837            info!("disabling plugin: {}", name);
2838            let mut store = match PluginStore::load() {
2839                Ok(s) => s,
2840                Err(e) => {
2841                    eprintln!("Error loading plugin store: {}", e);
2842                    return;
2843                }
2844            };
2845
2846            if let Err(e) = store.disable_plugin(&name) {
2847                eprintln!("Error disabling plugin: {}", e);
2848                return;
2849            }
2850
2851            if let Err(e) = store.save() {
2852                eprintln!("Error saving plugin store: {}", e);
2853                return;
2854            }
2855
2856            println!("Plugin '{}' disabled.", name);
2857        }
2858    }
2859}
2860
2861pub fn handle_debug(cmd: DebugCommand) {
2862    match cmd {
2863        DebugCommand::Share { lines, expire, local } => {
2864            info!("debug share: lines={}, expire={}, local={}", lines, expire, local);
2865            println!("Hermes Debug Share");
2866            println!("====================");
2867            println!();
2868            println!("Parameters:");
2869            println!("  Lines: {}", lines);
2870            println!("  Expire: {} days", expire);
2871            println!("  Local only: {}", local);
2872            println!();
2873
2874            // Gather debug information
2875            let hermes_home = Config::hermes_home();
2876            let config_path = Config::config_path();
2877
2878            println!("Debug Information:");
2879            println!("------------------");
2880            println!();
2881
2882            // Version info
2883            println!("Version: {}", env!("CARGO_PKG_VERSION"));
2884            println!();
2885
2886            // Paths
2887            println!("Paths:");
2888            println!("  HERMES_HOME: {:?}", hermes_home);
2889            println!("  Config: {:?}", config_path);
2890            println!();
2891
2892            // Config summary (last {} lines of config if exists)
2893            if config_path.exists() {
2894                if let Ok(content) = fs::read_to_string(&config_path) {
2895                    let config_lines: Vec<&str> =
2896                        content.lines().rev().take(lines as usize).collect();
2897                    println!("Config (last {} lines):", config_lines.len());
2898                    for line in config_lines.iter().rev() {
2899                        println!("  {}", line);
2900                    }
2901                }
2902            }
2903            println!();
2904
2905            // Auth summary (no secrets)
2906            let auth_store = AuthStore::load().unwrap_or_default();
2907            println!("Auth providers: {}", auth_store.credentials.len());
2908            for cred in &auth_store.credentials {
2909                println!("  - {}", cred.provider);
2910            }
2911            println!();
2912
2913            // Tool status
2914            let tools = tools::list_tools(false).unwrap_or_default();
2915            println!("Tools: {} registered", tools.len());
2916            let enabled = tools.iter().filter(|(_, _, _, e)| *e).count();
2917            println!("  {} enabled, {} disabled", enabled, tools.len() - enabled);
2918
2919            if local {
2920                println!();
2921                println!("[LOCAL MODE] Debug info printed to stdout only.");
2922                println!("No data was shared or transmitted.");
2923            } else {
2924                println!();
2925                println!("[REMOTE MODE] Note: Actual sharing functionality not implemented.");
2926                println!("This would upload debug info to a temporary paste service.");
2927            }
2928        }
2929    }
2930}
2931
2932pub fn handle_claw(cmd: ClawCommand) {
2933    match cmd {
2934        ClawCommand::Migrate {
2935            source,
2936            dry_run,
2937            preset,
2938            overwrite,
2939            migrate_secrets,
2940            workspace_target: _,
2941            skill_conflict,
2942            yes,
2943        } => {
2944            info!("claw migrate: source={:?}, dry_run={}", source, dry_run);
2945            println!("Hermes Claw Migrate");
2946            println!("====================");
2947            println!();
2948
2949            let source_path = source.clone().unwrap_or_else(|| ".".to_string());
2950            println!("Source: {}", source_path);
2951            println!("Preset: {}", preset);
2952            println!("Dry run: {}", dry_run);
2953            println!();
2954
2955            // Show what would be migrated
2956            println!("Migration would process:");
2957            println!("  - Skills configuration");
2958            println!(
2959                "  - Auth credentials {}",
2960                if migrate_secrets { "(including secrets)" } else { "(secrets excluded)" }
2961            );
2962            println!("  - Config settings");
2963            println!("  - Tool configurations");
2964            println!();
2965
2966            if overwrite {
2967                println!("[WARNING] --overwrite is set. Existing data will be replaced.");
2968                println!();
2969            }
2970
2971            match skill_conflict.as_str() {
2972                "skip" => println!("Skill conflicts: skip"),
2973                "overwrite" => println!("Skill conflicts: overwrite"),
2974                "keep" => println!("Skill conflicts: keep existing"),
2975                _ => println!("Skill conflicts: {}", skill_conflict),
2976            }
2977            println!();
2978
2979            if dry_run {
2980                println!("[DRY RUN] No changes have been made.");
2981                println!("Run without --dry-run to perform the actual migration.");
2982            } else {
2983                if !yes {
2984                    println!("WARNING: This will modify your Hermes configuration.");
2985                    println!("Use --yes to confirm or --dry-run to preview first.");
2986                }
2987            }
2988        }
2989        ClawCommand::Cleanup { source, dry_run, yes } => {
2990            info!("claw cleanup: source={:?}, dry_run={}", source, dry_run);
2991            println!("Hermes Claw Cleanup");
2992            println!("====================");
2993            println!();
2994
2995            let source_path = source.clone().unwrap_or_else(|| ".".to_string());
2996            println!("Source: {}", source_path);
2997            println!("Dry run: {}", dry_run);
2998            println!();
2999
3000            // Show what would be cleaned up
3001            println!("Cleanup would remove:");
3002            println!("  - Orphaned skill directories");
3003            println!("  - Unused configuration keys");
3004            println!("  - Temporary files");
3005            println!("  - Cache directories");
3006            println!();
3007
3008            if dry_run {
3009                println!("[DRY RUN] No changes have been made.");
3010                println!("Run without --dry-run to perform the actual cleanup.");
3011            } else {
3012                if !yes {
3013                    println!("WARNING: This will delete files from your Hermes directory.");
3014                    println!("Use --yes to confirm or --dry-run to preview first.");
3015                }
3016            }
3017        }
3018    }
3019}
3020
3021// ── Backup / Import / Dump ─────────────────────────────────────────────────────
3022
3023/// Handle the `hermes backup` command
3024pub fn handle_backup(output: Option<String>, quick: bool, label: Option<String>) -> Result<()> {
3025    use chrono::Local;
3026
3027    let hermes_home = Config::hermes_home();
3028    let backups_dir = hermes_home.join("backups");
3029
3030    let timestamp = Local::now().format("%Y%m%d-%H%M%S");
3031    let label_suffix = label.clone().map(|l| format!("-{}", l)).unwrap_or_default();
3032    let quick_suffix = if quick { "-quick" } else { "" };
3033    let backup_name = format!("hermes-backup-{}{}{}", timestamp, quick_suffix, label_suffix);
3034    let backup_path = backups_dir.join(&backup_name);
3035
3036    println!("Hermes Backup");
3037    println!("=============");
3038    println!();
3039    println!("Creating backup: {}", backup_name);
3040    println!("Source: {:?}", hermes_home);
3041    println!("Destination: {:?}", backup_path);
3042    println!();
3043
3044    fs::create_dir_all(&backups_dir)
3045        .with_context(|| format!("failed to create backups directory {:?}", backups_dir))?;
3046    fs::create_dir_all(&backup_path)
3047        .with_context(|| format!("failed to create backup directory {:?}", backup_path))?;
3048
3049    let items_to_backup: Vec<(&str, Option<&str>)> = if quick {
3050        vec![
3051            ("config.yaml", Some("config.yaml")),
3052            ("sessions.db", Some("sessions.db")),
3053            ("credentials.yaml", Some("auth.json")),
3054        ]
3055    } else {
3056        vec![
3057            ("config.yaml", Some("config.yaml")),
3058            ("sessions.db", Some("sessions.db")),
3059            ("credentials.yaml", Some("auth.json")),
3060            ("cron", None),
3061            ("memory", None),
3062            ("profiles", None),
3063            ("skills", None),
3064            (".env", Some(".env")),
3065        ]
3066    };
3067
3068    let mut backed_up_count = 0;
3069    let mut total_size: u64 = 0;
3070
3071    for (item_name, dest_name) in items_to_backup {
3072        let src = hermes_home.join(item_name);
3073        let dst = backup_path.join(dest_name.unwrap_or(item_name));
3074
3075        if !src.exists() {
3076            continue;
3077        }
3078
3079        if src.is_dir() {
3080            copy_dir_recursive(&src, &dst)?;
3081            let size = calculate_dir_size(&dst);
3082            total_size += size;
3083            println!("  [OK] Backed up directory: {} ({} bytes)", item_name, size);
3084        } else {
3085            fs::copy(&src, &dst).with_context(|| format!("failed to copy {:?}", src))?;
3086            let size = src.metadata().map(|m| m.len()).unwrap_or(0);
3087            total_size += size;
3088            println!("  [OK] Backed up file: {} ({} bytes)", item_name, size);
3089        }
3090        backed_up_count += 1;
3091    }
3092
3093    let metadata = BackupMetadata {
3094        version: env!("CARGO_PKG_VERSION").to_string(),
3095        timestamp: Local::now().to_rfc3339(),
3096        hermes_home: hermes_home.to_string_lossy().to_string(),
3097        quick,
3098        label,
3099        items_backed_up: backed_up_count,
3100        total_size_bytes: total_size,
3101    };
3102    let metadata_path = backup_path.join("backup-meta.yaml");
3103    let metadata_yaml = serde_yaml::to_string(&metadata)
3104        .with_context(|| "failed to serialize backup metadata".to_string())?;
3105    fs::write(&metadata_path, metadata_yaml)
3106        .with_context(|| format!("failed to write metadata to {:?}", metadata_path))?;
3107
3108    println!();
3109    println!("Backup complete!");
3110    println!("  {} item(s) backed up", backed_up_count);
3111    println!("  Total size: {} bytes", total_size);
3112    println!("  Location: {:?}", backup_path);
3113
3114    if let Some(custom_output) = output {
3115        println!("  Copy/symlink to: {}", custom_output);
3116    }
3117
3118    Ok(())
3119}
3120
3121/// Handle the `hermes import` command
3122pub fn handle_import(backup_path: String, force: bool) -> Result<()> {
3123    let hermes_home = Config::hermes_home();
3124    let backup_dir = PathBuf::from(&backup_path);
3125
3126    println!("Hermes Import");
3127    println!("=============");
3128    println!();
3129    println!("Backup source: {:?}", backup_dir);
3130    println!("Restore target: {:?}", hermes_home);
3131    println!();
3132
3133    if !backup_dir.exists() {
3134        anyhow::bail!("Backup directory does not exist: {:?}", backup_dir);
3135    }
3136
3137    let metadata_path = backup_dir.join("backup-meta.yaml");
3138    let has_metadata = metadata_path.exists();
3139
3140    let items_in_backup = get_backup_items(&backup_dir)?;
3141
3142    if items_in_backup.is_empty() {
3143        anyhow::bail!("Backup directory is empty or invalid: {:?}", backup_dir);
3144    }
3145
3146    println!("Items found in backup:");
3147    for item in &items_in_backup {
3148        println!("  - {}", item);
3149    }
3150    println!();
3151
3152    if has_metadata {
3153        match fs::read_to_string(&metadata_path) {
3154            Ok(content) => match serde_yaml::from_str::<BackupMetadata>(&content) {
3155                Ok(metadata) => {
3156                    println!("Backup metadata:");
3157                    println!("  Version: {}", metadata.version);
3158                    println!("  Created: {}", metadata.timestamp);
3159                    println!("  Size: {} bytes", metadata.total_size_bytes);
3160                    if metadata.quick {
3161                        println!("  Type: quick");
3162                    }
3163                    if let Some(ref l) = metadata.label {
3164                        println!("  Label: {}", l);
3165                    }
3166                    println!();
3167                }
3168                Err(e) => {
3169                    eprintln!("Warning: Could not parse backup metadata: {}", e);
3170                }
3171            },
3172            Err(e) => {
3173                eprintln!("Warning: Could not read backup metadata: {}", e);
3174            }
3175        }
3176    }
3177
3178    if !force {
3179        println!("WARNING: This will overwrite existing files in {:?}", hermes_home);
3180        println!("Continue? [y/N] ");
3181        let mut input = String::new();
3182        if std::io::stdin().read_line(&mut input).is_err() {
3183            anyhow::bail!("Failed to read confirmation input");
3184        }
3185        let input = input.trim().to_lowercase();
3186        if input != "y" && input != "yes" {
3187            println!("Import cancelled.");
3188            return Ok(());
3189        }
3190    }
3191
3192    let mut restored_count = 0;
3193    for item_name in &items_in_backup {
3194        let src = backup_dir.join(item_name);
3195        let dst = hermes_home.join(item_name);
3196
3197        if !src.exists() {
3198            continue;
3199        }
3200
3201        if let Some(parent) = dst.parent() {
3202            fs::create_dir_all(parent)
3203                .with_context(|| format!("failed to create directory {:?}", parent))?;
3204        }
3205
3206        if src.is_dir() {
3207            if dst.exists() {
3208                fs::remove_dir_all(&dst)
3209                    .with_context(|| format!("failed to remove existing directory {:?}", dst))?;
3210            }
3211            copy_dir_recursive(&src, &dst)?;
3212            println!("  [OK] Restored directory: {}", item_name);
3213        } else {
3214            fs::copy(&src, &dst).with_context(|| format!("failed to restore file {:?}", src))?;
3215            println!("  [OK] Restored file: {}", item_name);
3216        }
3217        restored_count += 1;
3218    }
3219
3220    println!();
3221    println!("Import complete!");
3222    println!("  {} item(s) restored", restored_count);
3223    println!("  Restored to: {:?}", hermes_home);
3224
3225    Ok(())
3226}
3227
3228/// Handle the `hermes dump` command
3229pub fn handle_dump(show_keys: bool) -> Result<()> {
3230    use std::env;
3231
3232    println!("========================================");
3233    println!("HERMES DIAGNOSTIC DUMP");
3234    println!("========================================");
3235    println!();
3236
3237    println!("-- Version --");
3238    println!("  Hermes CLI: {}", env!("CARGO_PKG_VERSION"));
3239    println!("  Rust: {} (target: {})", env::consts::ARCH, env::consts::OS);
3240
3241    println!();
3242    println!("-- OS Info --");
3243    #[cfg(target_os = "windows")]
3244    {
3245        println!("  OS: Windows");
3246        if let Ok(version) = env::var("OS") {
3247            println!("  OS Version: {}", version);
3248        }
3249    }
3250    #[cfg(not(target_os = "windows"))]
3251    {
3252        println!("  OS: {}", std::env::consts::OS);
3253    }
3254
3255    let hermes_home = Config::hermes_home();
3256    let config_path = Config::config_path();
3257    let auth_path = crate::auth::AuthStore::auth_path();
3258
3259    println!();
3260    println!("-- Paths --");
3261    println!("  HERMES_HOME: {:?}", hermes_home);
3262    println!("  Config: {:?}", config_path);
3263    println!("  Auth Store: {:?}", auth_path);
3264    if let Ok(profile) = env::var("HERMES_PROFILE") {
3265        println!("  HERMES_PROFILE: {}", profile);
3266    }
3267    if let Ok(home) = env::var("HERMES_HOME") {
3268        println!("  HERMES_HOME (env): {}", home);
3269    }
3270
3271    println!();
3272    println!("-- Disk Space --");
3273    #[cfg(target_os = "windows")]
3274    {
3275        let output = std::process::Command::new("powershell")
3276            .args(["-NoProfile", "-Command", "(Get-PSDrive C).Free / 1GB"])
3277            .output();
3278        if let Ok(o) = output {
3279            if o.status.success() {
3280                let free_gb =
3281                    String::from_utf8_lossy(&o.stdout).trim().parse::<f64>().unwrap_or(0.0);
3282                println!("  C: drive free: {:.2} GB", free_gb);
3283            }
3284        }
3285    }
3286
3287    println!();
3288    println!("-- Config --");
3289    match Config::load() {
3290        Ok(config) => {
3291            println!("  Model: {}", config.model.default);
3292            println!("  Provider: {}", config.model.provider);
3293            if !config.model.base_url.is_empty() {
3294                println!("  Base URL: {}", config.model.base_url);
3295            }
3296            println!("  Max turns: {}", config.agent.max_turns);
3297            println!("  Reasoning effort: {}", config.agent.reasoning_effort);
3298            println!("  Terminal env: {}", config.terminal.env_type);
3299            println!("  Timeout: {}s", config.terminal.timeout);
3300            println!("  Display streaming: {}", config.display.streaming);
3301        }
3302        Err(e) => {
3303            println!("  Error loading config: {}", e);
3304        }
3305    }
3306
3307    println!();
3308    println!("-- Auth Providers --");
3309    let auth_store = crate::auth::AuthStore::load()?;
3310    if auth_store.credentials.is_empty() {
3311        println!("  No providers configured");
3312    } else {
3313        for cred in &auth_store.credentials {
3314            let masked_key: String =
3315                if show_keys { cred.api_key.clone() } else { mask_key(&cred.api_key) };
3316            println!("  {}: {}", cred.provider, masked_key);
3317            if let Some(ref base_url) = cred.base_url {
3318                println!("    base_url: {}", base_url);
3319            }
3320        }
3321    }
3322
3323    println!();
3324    println!("-- Sessions --");
3325    let sessions_db_path = hermes_home.join("sessions.db");
3326    println!("  Database: {:?}", sessions_db_path);
3327    if sessions_db_path.exists() {
3328        if let Ok(meta) = fs::metadata(&sessions_db_path) {
3329            println!("  Size: {} bytes", meta.len());
3330        }
3331        println!("  Status: exists");
3332    } else {
3333        println!("  Status: not found");
3334    }
3335
3336    println!();
3337    println!("-- Tool Registry --");
3338    let tools = tools::get_builtin_tools();
3339    let mut toolsets: std::collections::HashSet<&str> = std::collections::HashSet::new();
3340    for t in &tools {
3341        toolsets.insert(t.toolset);
3342    }
3343    println!("  Built-in tools: {}", tools.len());
3344    println!("  Toolsets: {}", toolsets.len());
3345    let mut sorted_toolsets: Vec<_> = toolsets.iter().collect();
3346    sorted_toolsets.sort();
3347    for toolset in sorted_toolsets {
3348        let count = tools.iter().filter(|t| t.toolset == *toolset).count();
3349        println!("    {}: {} tool(s)", toolset, count);
3350    }
3351
3352    println!();
3353    println!("-- Cron --");
3354    let cron_dir = cron_mod::cron_dir();
3355    println!("  Directory: {:?}", cron_dir);
3356    if cron_dir.exists() {
3357        if let Ok(entries) = fs::read_dir(&cron_dir) {
3358            let count = entries.filter_map(|e| e.ok()).count();
3359            println!("  Entries: {}", count);
3360        }
3361        let jobs_path = cron_mod::cron_jobs_path();
3362        if jobs_path.exists() {
3363            println!("  Jobs file: exists");
3364        }
3365    } else {
3366        println!("  Status: not configured");
3367    }
3368
3369    println!();
3370    println!("-- Skills --");
3371    let skills_home = SkillsIndex::skills_home();
3372    println!("  Directory: {:?}", skills_home);
3373    if skills_home.exists() {
3374        match SkillsIndex::load() {
3375            Ok(index) => {
3376                println!("  Indexed skills: {}", index.skills.len());
3377            }
3378            Err(_) => {
3379                println!("  Could not load skills index");
3380            }
3381        }
3382    } else {
3383        println!("  Status: not installed");
3384    }
3385
3386    println!();
3387    println!("-- Environment Variables (HERMES_) --");
3388    for (key, value) in env::vars() {
3389        if key.starts_with("HERMES_") {
3390            println!("  {}: {}", key, value);
3391        }
3392    }
3393
3394    println!();
3395    println!("========================================");
3396    println!("End of diagnostic dump");
3397
3398    Ok(())
3399}
3400
3401// Helper functions
3402
3403#[derive(Debug, serde::Serialize, serde::Deserialize)]
3404struct BackupMetadata {
3405    version: String,
3406    timestamp: String,
3407    hermes_home: String,
3408    quick: bool,
3409    label: Option<String>,
3410    items_backed_up: usize,
3411    total_size_bytes: u64,
3412}
3413
3414fn get_backup_items(backup_dir: &Path) -> Result<Vec<String>> {
3415    let mut items = Vec::new();
3416    let expected_files = ["config.yaml", "sessions.db", "credentials.yaml", ".env"];
3417    let expected_dirs = ["cron", "memory", "profiles", "skills"];
3418
3419    for name in &expected_files {
3420        if backup_dir.join(name).exists() {
3421            items.push(name.to_string());
3422        }
3423    }
3424
3425    for name in &expected_dirs {
3426        if backup_dir.join(name).is_dir() {
3427            items.push(name.to_string());
3428        }
3429    }
3430
3431    Ok(items)
3432}
3433
3434fn copy_dir_recursive(src: &PathBuf, dst: &PathBuf) -> Result<()> {
3435    if !src.is_dir() {
3436        anyhow::bail!("Source is not a directory: {:?}", src);
3437    }
3438
3439    fs::create_dir_all(dst).with_context(|| format!("failed to create directory {:?}", dst))?;
3440
3441    for entry in fs::read_dir(src).with_context(|| format!("failed to read directory {:?}", src))? {
3442        let entry =
3443            entry.with_context(|| format!("failed to read directory entry in {:?}", src))?;
3444        let ty = entry
3445            .file_type()
3446            .with_context(|| format!("failed to get file type for {:?}", entry.path()))?;
3447        let src_path = entry.path();
3448        let dst_path = dst.join(entry.file_name());
3449
3450        if ty.is_dir() {
3451            copy_dir_recursive(&src_path, &dst_path)?;
3452        } else {
3453            fs::copy(&src_path, &dst_path)
3454                .with_context(|| format!("failed to copy {:?} to {:?}", src_path, dst_path))?;
3455        }
3456    }
3457
3458    Ok(())
3459}
3460
3461fn calculate_dir_size(path: &PathBuf) -> u64 {
3462    let mut size = 0u64;
3463    if let Ok(entries) = fs::read_dir(path) {
3464        for entry in entries.filter_map(|e| e.ok()) {
3465            if let Ok(meta) = entry.metadata() {
3466                if meta.is_dir() {
3467                    size += calculate_dir_size(&entry.path());
3468                } else {
3469                    size += meta.len();
3470                }
3471            }
3472        }
3473    }
3474    size
3475}
3476
3477fn mask_key(key: &str) -> String {
3478    if key.len() <= 8 {
3479        return "*".repeat(key.len());
3480    }
3481    let start = &key[..4];
3482    let end = &key[key.len() - 4..];
3483    format!("{}...{}", start, end)
3484}
3485
3486/// Handle the `hermes completion` command
3487pub fn handle_completion(shell: Option<&str>) {
3488    println!("Hermes Shell Completion");
3489    println!("========================");
3490    println!();
3491
3492    let shell = shell.unwrap_or("bash");
3493    let hermes_home = Config::hermes_home();
3494
3495    println!("Generating completion script for: {}", shell);
3496    println!();
3497
3498    match shell.to_lowercase().as_str() {
3499        "bash" => {
3500            println!("Add to your ~/.bashrc or ~/.bash_profile:");
3501            println!();
3502            println!("  source <(hermes --completion bash)");
3503        }
3504        "zsh" => {
3505            println!("Add to your ~/.zshrc:");
3506            println!();
3507            println!("  autoload -U compinit");
3508            println!("  compinit");
3509            println!("  source <(hermes --completion zsh)");
3510        }
3511        "fish" => {
3512            println!("Run:");
3513            println!();
3514            println!("  hermes --completion fish | source");
3515        }
3516        "powershell" | "pwsh" => {
3517            println!("Add to your PowerShell profile:");
3518            println!();
3519            println!("  hermes --completion powershell | Out-String | Invoke-Expression");
3520        }
3521        _ => {
3522            println!("Unsupported shell: {}. Supported: bash, zsh, fish, powershell", shell);
3523        }
3524    }
3525
3526    println!();
3527    println!("Hermes completion script location: {:?}", hermes_home.join("completion"));
3528}
3529
3530/// Handle the `hermes insights` command
3531pub fn handle_insights(days: u32, source: Option<&str>) -> Result<()> {
3532    use hermes_session_db::SessionStore;
3533
3534    println!("Hermes Insights");
3535    println!("==============");
3536    println!();
3537    println!("Analyzing last {} days of activity...", days);
3538    if let Some(s) = source {
3539        println!("Filter: source = {}", s);
3540    }
3541    println!();
3542
3543    let home = Config::hermes_home();
3544    let db_path = home.join("sessions.db");
3545
3546    if !db_path.exists() {
3547        println!("No session database found. Start chatting to generate insights!");
3548        return Ok(());
3549    }
3550
3551    let store = SessionStore::new(&db_path)
3552        .map_err(|e| anyhow::anyhow!("Failed to open session DB: {}", e))?;
3553
3554    let cutoff = chrono::Utc::now() - chrono::Duration::days(days as i64);
3555    let sessions = store
3556        .list_sessions(10000)
3557        .map_err(|e| anyhow::anyhow!("Failed to list sessions: {}", e))?;
3558
3559    // Filter by source if specified
3560    let filtered_sessions: Vec<_> = sessions
3561        .iter()
3562        .filter(|s| if let Some(src) = source { s.source == src } else { true })
3563        .filter(|s| s.updated_at >= cutoff)
3564        .collect();
3565
3566    if filtered_sessions.is_empty() {
3567        println!("No sessions found in the last {} days.", days);
3568        return Ok(());
3569    }
3570
3571    println!("Sessions: {}", filtered_sessions.len());
3572    println!();
3573
3574    // Calculate messages per day
3575    let mut messages_per_day: std::collections::HashMap<String, usize> =
3576        std::collections::HashMap::new();
3577    let mut total_messages = 0;
3578
3579    for session in &filtered_sessions {
3580        if let Ok(messages) = store.get_messages(&session.id) {
3581            total_messages += messages.len();
3582            let day = session.updated_at.format("%Y-%m-%d").to_string();
3583            *messages_per_day.entry(day).or_insert(0) += messages.len();
3584        }
3585    }
3586
3587    println!("Total messages: {}", total_messages);
3588    if !filtered_sessions.is_empty() {
3589        println!(
3590            "Avg messages/session: {:.1}",
3591            total_messages as f64 / filtered_sessions.len() as f64
3592        );
3593    }
3594    println!();
3595
3596    // Top sources
3597    let mut sources: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
3598    for session in &filtered_sessions {
3599        *sources.entry(&session.source).or_insert(0) += 1;
3600    }
3601    let mut top_sources: Vec<_> = sources.iter().collect();
3602    top_sources.sort_by(|a, b| b.1.cmp(a.1));
3603
3604    println!("Top sources:");
3605    for (src, count) in top_sources.iter().take(5) {
3606        println!("  {}: {} sessions", src, count);
3607    }
3608    println!();
3609
3610    // Message distribution by day (last 7 days)
3611    println!("Recent activity:");
3612    let now = chrono::Utc::now();
3613    for i in 0..7 {
3614        let day = (now - chrono::Duration::days(i)).format("%Y-%m-%d").to_string();
3615        let count = messages_per_day.get(&day).unwrap_or(&0);
3616        println!("  {}: {} messages", day, count);
3617    }
3618
3619    Ok(())
3620}
3621
3622/// Handle the `hermes login` command
3623#[allow(clippy::too_many_arguments)]
3624pub fn handle_login(
3625    provider: Option<&str>,
3626    portal_url: Option<&str>,
3627    inference_url: Option<&str>,
3628    client_id: Option<&str>,
3629    scope: Option<&str>,
3630    _no_browser: bool,
3631    timeout: f64,
3632    ca_bundle: Option<&str>,
3633    insecure: bool,
3634) -> Result<()> {
3635    println!("Hermes Login");
3636    println!("============");
3637    println!();
3638
3639    let provider = provider.unwrap_or("nous");
3640    println!("Provider: {}", provider);
3641    println!();
3642
3643    // Build the login URL
3644    let portal = portal_url.unwrap_or("https://portal.nousresearch.com");
3645    let login_path = "/auth/login";
3646
3647    println!("To login:");
3648    println!();
3649    println!("1. Open the following URL in your browser:");
3650    println!();
3651    println!("   {}{}", portal, login_path);
3652    println!();
3653
3654    println!("2. Complete the OAuth flow in your browser");
3655    println!("3. Copy the authorization code");
3656    println!();
3657
3658    println!("Configuration:");
3659    if let Some(inf_url) = inference_url {
3660        println!("  Inference URL: {}", inf_url);
3661    }
3662    if let Some(cid) = client_id {
3663        println!("  Client ID: {}", cid);
3664    }
3665    if let Some(sc) = scope {
3666        println!("  Scope: {}", sc);
3667    }
3668    println!("  Timeout: {}s", timeout);
3669    if insecure {
3670        println!("  [WARNING] TLS verification disabled");
3671    }
3672    if let Some(ca) = ca_bundle {
3673        println!("  CA Bundle: {}", ca);
3674    }
3675
3676    println!();
3677    println!("Then run:");
3678    println!("  hermes auth add {} --api-key <your-token>", provider);
3679
3680    Ok(())
3681}
3682
3683/// Handle the `hermes logout` command
3684pub fn handle_logout(provider: Option<&str>) -> Result<()> {
3685    println!("Hermes Logout");
3686    println!("=============");
3687    println!();
3688
3689    let mut store = AuthStore::load()?;
3690    let credentials = store.list();
3691
3692    if credentials.is_empty() {
3693        println!("No auth credentials configured.");
3694        return Ok(());
3695    }
3696
3697    if let Some(p) = provider {
3698        // Logout specific provider
3699        if store.remove(p) {
3700            store.save()?;
3701            println!("Logged out from {}.", p);
3702        } else {
3703            println!("No credentials found for provider: {}", p);
3704        }
3705    } else {
3706        // Logout all
3707        let count = store.credentials.len();
3708        store.reset();
3709        store.save()?;
3710        println!("Logged out from {} provider(s).", count);
3711    }
3712
3713    println!();
3714    println!("To login again, run:");
3715    println!("  hermes login");
3716
3717    Ok(())
3718}
3719
3720/// Handle the `hermes whatsapp` command
3721pub fn handle_whatsapp() -> Result<()> {
3722    println!("Hermes WhatsApp Setup");
3723    println!("=====================");
3724    println!();
3725
3726    println!("WhatsApp integration allows you to interact with Hermes via WhatsApp.");
3727    println!();
3728
3729    println!("Setup Instructions:");
3730    println!("------------------");
3731    println!();
3732    println!("1. Install hermes-gateway:");
3733    println!("   pip install hermes-agent");
3734    println!();
3735    println!("2. Configure WhatsApp gateway:");
3736    println!("   hermes gateway setup whatsapp");
3737    println!();
3738    println!("3. Link your WhatsApp number:");
3739    println!("   - Run: hermes gateway run -P whatsapp");
3740    println!("   - Scan the QR code with WhatsApp");
3741    println!();
3742    println!("4. Start chatting with Hermes on WhatsApp!");
3743    println!();
3744
3745    println!("Requirements:");
3746    println!("  - WhatsApp Business API account (optional, for official integration)");
3747    println!("  - Or use the Unofficial WhatsApp gateway (development)");
3748    println!();
3749
3750    println!("For more help:");
3751    println!("  hermes gateway setup");
3752
3753    Ok(())
3754}
3755
3756/// Handle the `hermes acp` command
3757pub fn handle_acp() -> Result<()> {
3758    println!("Hermes ACP Server Mode");
3759    println!("======================");
3760    println!();
3761
3762    println!("ACP (Agent Communication Protocol) enables Hermes to communicate");
3763    println!("with other agents and services in a distributed system.");
3764    println!();
3765
3766    println!("Server Modes:");
3767    println!("-------------");
3768    println!();
3769    println!("  1. Local Mode (default)");
3770    println!("     - Runs on localhost for single-user testing");
3771    println!("     - No network exposure");
3772    println!();
3773    println!("  2. Network Mode");
3774    println!("     - Exposes ACP server on network for multi-agent communication");
3775    println!("     - Requires authentication");
3776    println!();
3777    println!("  3. Gateway Mode");
3778    println!("     - Full gateway with ACP + platform integrations");
3779    println!("     - hermes gateway run");
3780    println!();
3781
3782    println!("Current Status:");
3783    let gateway_running = gateway_mod::is_gateway_running();
3784    if gateway_running {
3785        println!("  Gateway: RUNNING");
3786        if let Some(state) = gateway_mod::read_gateway_state() {
3787            println!(
3788                "  ACP: {} (state: {})",
3789                if state.gateway_state == "running" { "enabled" } else { "disabled" },
3790                state.gateway_state
3791            );
3792        }
3793    } else {
3794        println!("  Gateway: STOPPED");
3795        println!("  ACP: not active");
3796    }
3797    println!();
3798
3799    println!("To start ACP server:");
3800    println!("  hermes gateway run");
3801
3802    Ok(())
3803}
3804
3805/// Handle the `hermes dashboard` command
3806pub fn handle_dashboard(port: u16, host: String, no_open: bool) -> Result<()> {
3807    println!("Hermes Dashboard");
3808    println!("================");
3809    println!();
3810
3811    let url = format!("http://{}:{}", host, port);
3812    println!("Dashboard URL: {}", url);
3813    println!("Port: {}", port);
3814    println!("Host: {}", host);
3815    println!();
3816
3817    if !no_open {
3818        println!("Opening dashboard in default browser...");
3819
3820        #[cfg(target_os = "windows")]
3821        {
3822            std::process::Command::new("cmd").args(["/C", "start", "", &url]).spawn().ok();
3823        }
3824
3825        #[cfg(target_os = "macos")]
3826        {
3827            std::process::Command::new("open").arg(&url).spawn().ok();
3828        }
3829
3830        #[cfg(target_os = "linux")]
3831        {
3832            std::process::Command::new("xdg-open").arg(&url).spawn().ok();
3833        }
3834    }
3835
3836    println!();
3837    println!("Dashboard Features:");
3838    println!("  - Session history and management");
3839    println!("  - Tool usage analytics");
3840    println!("  - Cron job monitoring");
3841    println!("  - Auth provider management");
3842    println!("  - Skills marketplace");
3843    println!();
3844
3845    println!("Note: Dashboard server runs locally. Access is restricted to this machine.");
3846    println!("      Use --no-open to prevent automatic browser opening.");
3847
3848    Ok(())
3849}
3850
3851/// Handle the `hermes logs` command
3852pub fn handle_logs(
3853    log_name: Option<&str>,
3854    lines: u32,
3855    follow: bool,
3856    level: Option<&str>,
3857    session: Option<&str>,
3858    since: Option<&str>,
3859    component: Option<&str>,
3860) -> Result<()> {
3861    use std::io::{self, BufRead};
3862
3863    println!("Hermes Logs");
3864    println!("==========");
3865    println!();
3866
3867    let hermes_home = Config::hermes_home();
3868    let logs_dir = hermes_home.join("logs");
3869
3870    // Determine which log to show
3871    let log_name = log_name.unwrap_or("agent");
3872    let log_file = logs_dir.join(format!("{}.log", log_name));
3873
3874    // Show available logs if listing
3875    if log_name == "list" {
3876        println!("Available logs:");
3877        if logs_dir.exists() {
3878            if let Ok(entries) = fs::read_dir(&logs_dir) {
3879                for entry in entries.filter_map(|e| e.ok()) {
3880                    if let Some(name) = entry.path().file_name().and_then(|n| n.to_str()) {
3881                        println!("  - {}", name.replace(".log", ""));
3882                    }
3883                }
3884            }
3885        }
3886        println!();
3887        println!("Usage: hermes logs <name> [options]");
3888        return Ok(());
3889    }
3890
3891    if !log_file.exists() {
3892        println!("Log file not found: {:?}", log_file);
3893        println!();
3894        println!("Available logs:");
3895        if logs_dir.exists() {
3896            if let Ok(entries) = fs::read_dir(&logs_dir) {
3897                for entry in entries.filter_map(|e| e.ok()) {
3898                    if let Some(name) = entry.path().file_name().and_then(|n| n.to_str()) {
3899                        println!("  - {}", name.replace(".log", ""));
3900                    }
3901                }
3902            } else {
3903                println!("  (no logs directory found)");
3904            }
3905        } else {
3906            println!("  (no logs directory found)");
3907        }
3908        return Ok(());
3909    }
3910
3911    println!("Log file: {:?}", log_file);
3912    println!("Showing last {} lines", lines);
3913    if follow {
3914        println!("[Following mode - press Ctrl+C to stop]");
3915    }
3916    println!();
3917
3918    // Parse level filter
3919    let level_filter = level.map(|l| l.to_uppercase());
3920    let session_filter = session.map(|s| s.to_string());
3921    let since_filter = since.map(|s| s.to_string());
3922
3923    // For simple viewing, just read and display the file
3924    if follow {
3925        // Follow mode - watch the file
3926        use std::io::Seek;
3927        let file = fs::File::open(&log_file)?;
3928        let reader = io::BufReader::new(file);
3929
3930        // Read existing lines first
3931        for line in reader.lines().take_while(|l| l.is_ok()).skip(lines as usize).flatten() {
3932            if !filter_line(
3933                &line,
3934                level_filter.as_deref(),
3935                session_filter.as_deref(),
3936                since_filter.as_deref(),
3937                component,
3938            ) {
3939                println!("{}", line);
3940            }
3941        }
3942
3943        // Then watch for new lines
3944        let file = fs::File::open(&log_file)?;
3945        let mut reader = io::BufReader::new(file);
3946        let mut seek_pos = reader.stream_position()?;
3947
3948        loop {
3949            use std::time::Duration;
3950            std::thread::sleep(Duration::from_millis(500));
3951
3952            let metadata = fs::metadata(&log_file)?;
3953            let current_size = metadata.len();
3954
3955            if current_size > seek_pos {
3956                let mut file = fs::File::open(&log_file)?;
3957                use std::io::Seek;
3958                file.seek(io::SeekFrom::Start(seek_pos))?;
3959                let reader = io::BufReader::new(file);
3960
3961                for line in reader.lines().map_while(Result::ok) {
3962                    if !filter_line(
3963                        &line,
3964                        level_filter.as_deref(),
3965                        session_filter.as_deref(),
3966                        since_filter.as_deref(),
3967                        component,
3968                    ) {
3969                        println!("{}", line);
3970                    }
3971                }
3972                seek_pos = current_size;
3973            }
3974        }
3975    } else {
3976        // Non-follow mode - just show last N lines
3977        let file = fs::File::open(&log_file)?;
3978        let reader = io::BufReader::new(file);
3979
3980        let all_lines: Vec<String> = reader.lines().map_while(Result::ok).collect();
3981        let start =
3982            if all_lines.len() > lines as usize { all_lines.len() - lines as usize } else { 0 };
3983
3984        for line in all_lines.iter().skip(start) {
3985            if !filter_line(
3986                line,
3987                level_filter.as_deref(),
3988                session_filter.as_deref(),
3989                since_filter.as_deref(),
3990                component,
3991            ) {
3992                println!("{}", line);
3993            }
3994        }
3995    }
3996
3997    Ok(())
3998}
3999
4000fn filter_line(
4001    line: &str,
4002    level: Option<&str>,
4003    _session: Option<&str>,
4004    _since: Option<&str>,
4005    component: Option<&str>,
4006) -> bool {
4007    // Filter by level
4008    if let Some(lvl) = level {
4009        if !line.contains(&format!("[{}]", lvl))
4010            && !line.to_uppercase().contains(&format!("{}:", lvl))
4011        {
4012            // Line doesn't contain the level
4013        }
4014    }
4015
4016    // Filter by component
4017    if let Some(comp) = component {
4018        if !line.contains(&format!("[{}]", comp)) && !line.contains(&format!("{}:", comp)) {
4019            // Line doesn't contain the component
4020        }
4021    }
4022
4023    false // Don't filter
4024}