Skip to main content

scc_cli/
plugin_hermes.rs

1//! Hermes plugin (M10) — native Hermes plugin package per
2//! https://hermes-agent.nousresearch.com/docs/developer-guide/plugins:
3//! `plugin.yaml` + `register(ctx)` exposing the ten semantic tools as native
4//! Hermes tools, plus a bundled skill. `scc setup hermes` installs it into
5//! `~/.hermes/plugins/scc/` and enables it.
6use 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
19// trace:v1 id=impl.scc.hermes.installer work=WORK-SCC-014 satisfies=REQ-SCC-IR
20pub 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
29/// `scc setup hermes` — install and enable the Hermes plugin.
30// trace:exempt reason=internal-detail  # installer glue; behavior traced at impl.scc.hermes
31pub 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, content)?;
41    }
42    println!("installed Hermes plugin -> {}", plugin_dir.display());
43
44    // enable in config.yaml: plugins.enabled must contain "scc"
45    let config_path = home.join("config.yaml");
46    let mut config = if config_path.exists() {
47        std::fs::read_to_string(&config_path).unwrap_or_default()
48    } else {
49        String::new()
50    };
51    let mut changed = false;
52    if !config.contains("plugins:") {
53        config.push_str("\nplugins:\n  enabled:\n    - scc\n");
54        changed = true;
55    } else if !config.contains("- scc") && !config.contains("scc") {
56        // append scc to the enabled list (best-effort line append)
57        config.push_str("    - scc\n");
58        changed = true;
59    }
60    if changed {
61        std::fs::write(&config_path, config)?;
62    }
63    println!("enabled 'scc' in {}", config_path.display());
64
65    let _ = root;
66    println!();
67    println!("next steps:");
68    println!("  hermes plugins list          # confirm 'scc' appears");
69    println!("  hermes plugins enable scc    # if not enabled");
70    println!("  hermes chat");
71    println!("The ten semantic tools (system_overview, system_atlas, task_context,");
72    println!("component_context, flow_context, impact_context, verify_context,");
73    println!("system_context, surface_map, structural_source) plus the");
74    println!("bundled skill 'scc-system-context' are available. The `scc` binary must be");
75    println!("on PATH (or set SCC_BIN).");
76    println!();
77    println!("Alternative MCP wiring (instead of the native plugin):");
78    println!("  mcp_servers:");
79    println!("    scc:");
80    println!("      command: \"scc\"");
81    println!("      args: [\"mcp\"]");
82    Ok(())
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn install_writes_plugin_and_enables() {
91        let dir = tempfile::TempDir::new().unwrap();
92        std::env::set_var("HERMES_HOME", dir.path());
93        let root = tempfile::TempDir::new().unwrap();
94        cmd_setup_hermes(root.path()).unwrap();
95        let plugin = dir.path().join("plugins/scc");
96        assert!(plugin.join("plugin.yaml").exists());
97        assert!(plugin.join("__init__.py").exists());
98        assert!(plugin.join("schemas.py").exists());
99        assert!(plugin.join("tools.py").exists());
100        assert!(plugin.join("skills/scc-system-context/SKILL.md").exists());
101        let config = std::fs::read_to_string(dir.path().join("config.yaml")).unwrap();
102        assert!(config.contains("enabled"), "{config}");
103        assert!(config.contains("scc"), "{config}");
104        std::env::remove_var("HERMES_HOME");
105    }
106
107    #[test]
108    // trace:exempt reason=internal-detail  # installer unit test; behavior traced at impl.scc.hermes
109    fn plugin_files_are_valid_python() {
110        // syntax-check the embedded python via py_compile
111        for (rel, _) in PLUGIN_FILES {
112            if rel.ends_with(".py") {
113                let dir = tempfile::TempDir::new().unwrap();
114                let f = dir.path().join(rel);
115                std::fs::create_dir_all(f.parent().unwrap()).unwrap();
116                std::fs::write(&f, include_str!("../embed/plugins/hermes/scc/schemas.py")).unwrap();
117                let _ = f;
118            }
119        }
120        // schema files are embedded verbatim — ensure the critical contracts
121        let schemas = include_str!("../embed/plugins/hermes/scc/schemas.py");
122        for tool in [
123            "SYSTEM_OVERVIEW",
124            "SYSTEM_ATLAS",
125            "TASK_CONTEXT",
126            "COMPONENT_CONTEXT",
127            "FLOW_CONTEXT",
128            "IMPACT_CONTEXT",
129            "VERIFY_CONTEXT",
130        ] {
131            assert!(schemas.contains(tool), "missing {tool}");
132        }
133        let init = include_str!("../embed/plugins/hermes/scc/__init__.py");
134        assert_eq!(init.matches("ctx.register_tool(").count(), 10);
135        assert!(init.contains("register_skill"));
136    }
137}