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 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 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 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 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 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 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 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
229fn 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
240fn 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; }
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 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; }
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 if !skill_path.join("SKILL.md").exists() {
446 skill_issues.push("missing SKILL.md");
447 }
448
449 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 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 let mut issues = Vec::new();
529
530 if !skill_md.exists() {
532 issues.push("missing SKILL.md");
533 }
534
535 if skill.name.contains("..")
537 || skill.name.contains('/')
538 || skill.name.contains('\\')
539 {
540 issues.push("skill name contains path separators");
541 }
542
543 if skill.description.is_empty() {
545 issues.push("empty description");
546 }
547
548 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 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 if let Err(e) = gateway_mod::write_pid_file() {
718 eprintln!("Warning: Could not write PID file: {}", e);
719 }
720
721 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 println!("Running uninstall (keeping data)...");
1786 println!();
1787
1788 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 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
1815pub 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 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 let _ = (session_id, title);
1940 println!("Session rename not yet supported in current schema.");
1941 }
1942 SessionsCommand::Browse { source: _, limit } => {
1943 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(¤t) {
2025 profiles::clone_profile(¤t, &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 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 let settings = read_memory_json("settings")?;
2349 let enabled = settings.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true);
2350
2351 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 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, 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 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 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 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 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 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(), 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 let new_version = "1.1.0".to_string(); 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 let hermes_home = Config::hermes_home();
2876 let config_path = Config::config_path();
2877
2878 println!("Debug Information:");
2879 println!("------------------");
2880 println!();
2881
2882 println!("Version: {}", env!("CARGO_PKG_VERSION"));
2884 println!();
2885
2886 println!("Paths:");
2888 println!(" HERMES_HOME: {:?}", hermes_home);
2889 println!(" Config: {:?}", config_path);
2890 println!();
2891
2892 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 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 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 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 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
3021pub 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
3121pub 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
3228pub 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#[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
3486pub 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
3530pub 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 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 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 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 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#[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 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
3683pub 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 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 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
3720pub 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
3756pub 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
3805pub 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
3851pub 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 let log_name = log_name.unwrap_or("agent");
3872 let log_file = logs_dir.join(format!("{}.log", log_name));
3873
3874 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 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 if follow {
3925 use std::io::Seek;
3927 let file = fs::File::open(&log_file)?;
3928 let reader = io::BufReader::new(file);
3929
3930 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 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 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 if let Some(lvl) = level {
4009 if !line.contains(&format!("[{}]", lvl))
4010 && !line.to_uppercase().contains(&format!("{}:", lvl))
4011 {
4012 }
4014 }
4015
4016 if let Some(comp) = component {
4018 if !line.contains(&format!("[{}]", comp)) && !line.contains(&format!("{}:", comp)) {
4019 }
4021 }
4022
4023 false }