1use std::path::Path;
7
8const PLUGIN_FILES: &[(&str, &str)] = &[
9 ("plugin.yaml", include_str!("../embed/plugins/hermes/scc/plugin.yaml")),
10 ("schemas.py", include_str!("../embed/plugins/hermes/scc/schemas.py")),
11 ("tools.py", include_str!("../embed/plugins/hermes/scc/tools.py")),
12 ("__init__.py", include_str!("../embed/plugins/hermes/scc/__init__.py")),
13 (
14 "skills/scc-system-context/SKILL.md",
15 include_str!("../embed/plugins/hermes/scc/skills/scc-system-context/SKILL.md"),
16 ),
17];
18
19pub fn hermes_home() -> std::path::PathBuf {
21 match std::env::var("HERMES_HOME") {
22 Ok(h) if !h.is_empty() => std::path::PathBuf::from(h),
23 _ => std::env::home_dir()
24 .unwrap_or_else(|| std::path::PathBuf::from("."))
25 .join(".hermes"),
26 }
27}
28
29pub fn cmd_setup_hermes(root: &Path) -> crate::Result<()> {
32 let home = hermes_home();
33 let plugin_dir = home.join("plugins/scc");
34 std::fs::create_dir_all(&plugin_dir)?;
35 for (rel, content) in PLUGIN_FILES {
36 let path = plugin_dir.join(rel);
37 if let Some(parent) = path.parent() {
38 std::fs::create_dir_all(parent)?;
39 }
40 std::fs::write(&path, crate::plugin_omp::strip_authoring_markers(content))?;
43 }
44 println!("installed Hermes plugin -> {}", plugin_dir.display());
45
46 let config_path = home.join("config.yaml");
48 let mut config = if config_path.exists() {
49 std::fs::read_to_string(&config_path).unwrap_or_default()
50 } else {
51 String::new()
52 };
53 let mut changed = false;
54 if !config.contains("plugins:") {
55 config.push_str("\nplugins:\n enabled:\n - scc\n");
56 changed = true;
57 } else if !config.contains("- scc") && !config.contains("scc") {
58 config.push_str(" - scc\n");
60 changed = true;
61 }
62 if changed {
63 std::fs::write(&config_path, config)?;
64 }
65 println!("enabled 'scc' in {}", config_path.display());
66
67 let _ = root;
68 println!();
69 println!("next steps:");
70 println!(" hermes plugins list # confirm 'scc' appears");
71 println!(" hermes plugins enable scc # if not enabled");
72 println!(" hermes chat");
73 println!("The ten semantic tools (system_overview, system_atlas, task_context,");
74 println!("component_context, flow_context, impact_context, verify_context,");
75 println!("system_context, surface_map, structural_source) plus the");
76 println!("bundled skill 'scc-system-context' are available. The `scc` binary must be");
77 println!("on PATH (or set SCC_BIN).");
78 println!();
79 println!("Alternative MCP wiring (instead of the native plugin):");
80 println!(" mcp_servers:");
81 println!(" scc:");
82 println!(" command: \"scc\"");
83 println!(" args: [\"mcp\"]");
84 Ok(())
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 #[test]
92 fn install_writes_plugin_and_enables() {
94 let dir = tempfile::TempDir::new().unwrap();
95 std::env::set_var("HERMES_HOME", dir.path());
96 let root = tempfile::TempDir::new().unwrap();
97 cmd_setup_hermes(root.path()).unwrap();
98 let plugin = dir.path().join("plugins/scc");
99 assert!(plugin.join("plugin.yaml").exists());
100 assert!(plugin.join("__init__.py").exists());
101 assert!(plugin.join("schemas.py").exists());
102 assert!(plugin.join("tools.py").exists());
103 assert!(plugin.join("skills/scc-system-context/SKILL.md").exists());
104 let config = std::fs::read_to_string(dir.path().join("config.yaml")).unwrap();
105 assert!(config.contains("enabled"), "{config}");
106 assert!(config.contains("scc"), "{config}");
107 for rel in ["plugin.yaml", "__init__.py", "tools.py", "skills/scc-system-context/SKILL.md"] {
109 let text = std::fs::read_to_string(plugin.join(rel)).unwrap();
110 assert!(!text.contains("trace:v1 id="), "markers stripped in installed {rel}");
111 }
112 std::env::remove_var("HERMES_HOME");
113 }
114
115 #[test]
116 fn plugin_files_are_valid_python() {
118 for (rel, _) in PLUGIN_FILES {
120 if rel.ends_with(".py") {
121 let dir = tempfile::TempDir::new().unwrap();
122 let f = dir.path().join(rel);
123 std::fs::create_dir_all(f.parent().unwrap()).unwrap();
124 std::fs::write(&f, include_str!("../embed/plugins/hermes/scc/schemas.py")).unwrap();
125 let _ = f;
126 }
127 }
128 let schemas = include_str!("../embed/plugins/hermes/scc/schemas.py");
130 for tool in [
131 "SYSTEM_OVERVIEW",
132 "SYSTEM_ATLAS",
133 "TASK_CONTEXT",
134 "COMPONENT_CONTEXT",
135 "FLOW_CONTEXT",
136 "IMPACT_CONTEXT",
137 "VERIFY_CONTEXT",
138 ] {
139 assert!(schemas.contains(tool), "missing {tool}");
140 }
141 let init = include_str!("../embed/plugins/hermes/scc/__init__.py");
142 assert_eq!(init.matches("ctx.register_tool(").count(), 10);
143 assert!(init.contains("register_skill"));
144 }
145}