Skip to main content

cai_plugin/
lib.rs

1//! CAI Plugin - Claude Code plugin interface
2//!
3//! This crate provides the WASM plugin interface for Claude Code integration.
4
5#![warn(missing_docs, unused_crate_dependencies)]
6
7use std::collections::HashMap;
8
9pub use cai_core::Result;
10
11/// Plugin configuration
12#[derive(Debug, Clone)]
13pub struct PluginConfig {
14    /// Plugin version
15    pub version: String,
16    /// Supported skills
17    pub skills: Vec<String>,
18    /// Supported commands
19    pub commands: Vec<String>,
20}
21
22impl Default for PluginConfig {
23    fn default() -> Self {
24        Self {
25            version: env!("CARGO_PKG_VERSION").to_string(),
26            skills: vec![
27                "cai.query".into(),
28                "cai.ingest".into(),
29                "cai.stats".into(),
30                "cai.tui".into(),
31                "cai.web".into(),
32            ],
33            commands: vec![
34                "cai.query".into(),
35                "cai.ingest".into(),
36                "cai.stats".into(),
37                "cai.tui".into(),
38                "cai.web".into(),
39            ],
40        }
41    }
42}
43
44/// Plugin trait for Claude Code integration
45pub trait Plugin {
46    /// Get plugin configuration
47    fn config(&self) -> &PluginConfig;
48
49    /// Handle a skill invocation
50    fn handle_skill(&mut self, skill: &str, params: &serde_json::Value) -> Result<String>;
51
52    /// Handle a command invocation
53    fn handle_command(&mut self, cmd: &str, args: &[String]) -> Result<String>;
54
55    /// Session start hook
56    fn on_session_start(&mut self, ctx: &SessionContext) -> Result<()>;
57
58    /// Session end hook
59    fn on_session_end(&mut self, ctx: &SessionContext) -> Result<()>;
60}
61
62/// Session context for hooks
63#[derive(Debug, Clone)]
64pub struct SessionContext {
65    /// Session ID
66    pub session_id: String,
67    /// Working directory
68    pub work_dir: String,
69    /// Environment variables
70    pub env: HashMap<String, String>,
71}
72
73/// Default plugin implementation
74pub struct CaiPlugin {
75    config: PluginConfig,
76}
77
78impl Default for CaiPlugin {
79    fn default() -> Self {
80        Self::new()
81    }
82}
83
84impl CaiPlugin {
85    /// Create a new plugin instance
86    pub fn new() -> Self {
87        Self {
88            config: PluginConfig::default(),
89        }
90    }
91
92    /// Get plugin info as JSON
93    pub fn info(&self) -> serde_json::Value {
94        serde_json::json!({
95            "name": "cai",
96            "version": self.config.version,
97            "skills": self.config.skills,
98            "commands": self.config.commands,
99        })
100    }
101}
102
103impl Plugin for CaiPlugin {
104    fn config(&self) -> &PluginConfig {
105        &self.config
106    }
107
108    fn handle_skill(&mut self, skill: &str, params: &serde_json::Value) -> Result<String> {
109        match skill {
110            "cai.query" => self.handle_query(params),
111            "cai.ingest" => self.handle_ingest(params),
112            "cai.stats" => self.handle_stats(params),
113            "cai.tui" => self.handle_tui(params),
114            "cai.web" => self.handle_web(params),
115            _ => Err(cai_core::Error::Message(format!(
116                "Unknown skill: {}",
117                skill
118            ))),
119        }
120    }
121
122    fn handle_command(&mut self, cmd: &str, args: &[String]) -> Result<String> {
123        // Delegate to CLI
124        match cmd {
125            "cai.query" | "cai.ingest" | "cai.stats" | "cai.tui" | "cai.web" => {
126                Ok(format!("Command delegated: {} {:?}", cmd, args))
127            }
128            _ => Err(cai_core::Error::Message(format!(
129                "Unknown command: {}",
130                cmd
131            ))),
132        }
133    }
134
135    fn on_session_start(&mut self, _ctx: &SessionContext) -> Result<()> {
136        // Initialize data store
137        Ok(())
138    }
139
140    fn on_session_end(&mut self, _ctx: &SessionContext) -> Result<()> {
141        // Persist analytics
142        Ok(())
143    }
144}
145
146impl CaiPlugin {
147    fn handle_query(&mut self, params: &serde_json::Value) -> Result<String> {
148        let sql = params["sql"]
149            .as_str()
150            .ok_or_else(|| cai_core::Error::Message("Missing 'sql' parameter".into()))?;
151        let output = params["output"].as_str().unwrap_or("table");
152        Ok(format!("Query: {} (output: {})", sql, output))
153    }
154
155    fn handle_ingest(&mut self, params: &serde_json::Value) -> Result<String> {
156        let source = params["source"]
157            .as_str()
158            .ok_or_else(|| cai_core::Error::Message("Missing 'source' parameter".into()))?;
159        Ok(format!("Ingest from: {}", source))
160    }
161
162    fn handle_stats(&mut self, _params: &serde_json::Value) -> Result<String> {
163        Ok("Statistics".to_string())
164    }
165
166    fn handle_tui(&mut self, _params: &serde_json::Value) -> Result<String> {
167        Ok("Launch TUI".to_string())
168    }
169
170    fn handle_web(&mut self, _params: &serde_json::Value) -> Result<String> {
171        Ok("Launch web dashboard".to_string())
172    }
173}
174
175/// Create a new CAI plugin instance
176///
177/// Returns a pointer to a heap-allocated `CaiPlugin` that must be freed
178/// using `cai_plugin_destroy` to avoid memory leaks.
179///
180/// # Safety
181///
182/// - The returned pointer must be freed with `cai_plugin_destroy`
183/// - The pointer is valid until `cai_plugin_destroy` is called
184#[no_mangle]
185pub extern "C" fn cai_plugin_create() -> *mut CaiPlugin {
186    Box::into_raw(Box::new(CaiPlugin::new()))
187}
188
189/// Destroy a CAI plugin instance
190///
191/// # Safety
192///
193/// - `ptr` must be a valid pointer returned by `cai_plugin_create` or null
194/// - This function should only be called once per plugin instance
195/// - The pointer becomes invalid after this call
196#[no_mangle]
197pub unsafe extern "C" fn cai_plugin_destroy(ptr: *mut CaiPlugin) {
198    unsafe {
199        if !ptr.is_null() {
200            let _ = Box::from_raw(ptr);
201        }
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn test_plugin_config() {
211        let plugin = CaiPlugin::new();
212        assert_eq!(plugin.config().skills.len(), 5);
213        assert!(plugin.config().skills.contains(&"cai.query".to_string()));
214    }
215
216    #[test]
217    fn test_handle_query() {
218        let mut plugin = CaiPlugin::new();
219        let params = serde_json::json!({"sql": "SELECT * FROM entries"});
220        let result = plugin.handle_skill("cai.query", &params);
221        assert!(result.is_ok());
222    }
223}