1use crate::{
9 config::{GLOBAL_CONFIG_DIR, WORK_DIR},
10 hook::{
11 mcp::{CallMcpToolInput, McpHandler, SearchMcpInput},
12 os::OsHook,
13 skill::{LoadSkillInput, SearchSkillInput, SkillHandler, loader},
14 },
15};
16use memory::InMemory;
17use wcore::{
18 AgentConfig, AgentEvent, Hook, Memory, RecallInput, RecallOptions, RememberInput, ToolRegistry,
19 model::Tool,
20};
21
22pub mod mcp;
23pub mod os;
24pub mod skill;
25
26pub struct DaemonHook {
32 pub memory: InMemory,
33 pub skills: SkillHandler,
34 pub mcp: McpHandler,
35 pub os: OsHook,
36}
37
38impl DaemonHook {
39 pub fn new(memory: InMemory, skills: SkillHandler, mcp: McpHandler) -> Self {
41 Self {
42 memory,
43 skills,
44 mcp,
45 os: OsHook::new(GLOBAL_CONFIG_DIR.join(WORK_DIR)),
46 }
47 }
48
49 pub async fn dispatch_tool(&self, name: &str, args: &str) -> String {
55 match name {
56 "remember" => self.dispatch_remember(args).await,
57 "recall" => self.dispatch_recall(args).await,
58 "search_mcp" => self.dispatch_search_mcp(args).await,
59 "call_mcp_tool" => self.dispatch_call_mcp_tool(args).await,
60 "search_skill" => self.dispatch_search_skill(args).await,
61 "load_skill" => self.dispatch_load_skill(args).await,
62 "read" => self.os.dispatch_read(args).await,
63 "write" => self.os.dispatch_write(args).await,
64 "bash" => self.os.dispatch_bash(args).await,
65 name => {
66 tracing::debug!(tool = name, "forwarding tool to MCP bridge");
67 let bridge = self.mcp.bridge().await;
68 bridge.call(name, args).await
69 }
70 }
71 }
72
73 async fn dispatch_remember(&self, args: &str) -> String {
76 let input: RememberInput = match serde_json::from_str(args) {
77 Ok(v) => v,
78 Err(e) => return format!("invalid arguments: {e}"),
79 };
80 if input.key.is_empty() {
81 return "missing required field: key".to_owned();
82 }
83 let key = input.key.clone();
84 match self.memory.store(input.key, input.value).await {
85 Ok(()) => format!("remembered: {key}"),
86 Err(e) => format!("failed to store: {e}"),
87 }
88 }
89
90 async fn dispatch_recall(&self, args: &str) -> String {
91 let input: RecallInput = match serde_json::from_str(args) {
92 Ok(v) => v,
93 Err(e) => return format!("invalid arguments: {e}"),
94 };
95 let limit = input.limit.unwrap_or(10) as usize;
96 let options = RecallOptions {
97 limit,
98 ..Default::default()
99 };
100 match self.memory.recall(&input.query, options).await {
101 Ok(entries) if entries.is_empty() => "no memories found".to_owned(),
102 Ok(entries) => entries
103 .iter()
104 .map(|e| format!("{}: {}", e.key, e.value))
105 .collect::<Vec<_>>()
106 .join("\n"),
107 Err(e) => format!("recall failed: {e}"),
108 }
109 }
110
111 async fn dispatch_search_mcp(&self, args: &str) -> String {
114 let input: SearchMcpInput = match serde_json::from_str(args) {
115 Ok(v) => v,
116 Err(e) => return format!("invalid arguments: {e}"),
117 };
118 let query = input.query.to_lowercase();
119 let bridge = self.mcp.bridge().await;
120 let tools = bridge.tools().await;
121 let matches: Vec<String> = tools
122 .iter()
123 .filter(|t| {
124 t.name.to_lowercase().contains(&query)
125 || t.description.to_lowercase().contains(&query)
126 })
127 .map(|t| format!("{}: {}", t.name, t.description))
128 .collect();
129 if matches.is_empty() {
130 "no tools found".to_owned()
131 } else {
132 matches.join("\n")
133 }
134 }
135
136 async fn dispatch_call_mcp_tool(&self, args: &str) -> String {
137 let input: CallMcpToolInput = match serde_json::from_str(args) {
138 Ok(v) => v,
139 Err(e) => return format!("invalid arguments: {e}"),
140 };
141 let tool_args = input.args.unwrap_or_default();
142 let bridge = self.mcp.bridge().await;
143 bridge.call(&input.name, &tool_args).await
144 }
145
146 async fn dispatch_search_skill(&self, args: &str) -> String {
149 let input: SearchSkillInput = match serde_json::from_str(args) {
150 Ok(v) => v,
151 Err(e) => return format!("invalid arguments: {e}"),
152 };
153 let query = input.query.to_lowercase();
154 let registry = self.skills.registry.lock().await;
155 let matches: Vec<String> = registry
156 .skills()
157 .into_iter()
158 .filter(|s| {
159 s.name.to_lowercase().contains(&query)
160 || s.description.to_lowercase().contains(&query)
161 })
162 .map(|s| format!("{}: {}", s.name, s.description))
163 .collect();
164 if matches.is_empty() {
165 "no skills found".to_owned()
166 } else {
167 matches.join("\n")
168 }
169 }
170
171 async fn dispatch_load_skill(&self, args: &str) -> String {
172 let input: LoadSkillInput = match serde_json::from_str(args) {
173 Ok(v) => v,
174 Err(e) => return format!("invalid arguments: {e}"),
175 };
176 let name = &input.name;
177 if name.contains("..") || name.contains('/') || name.contains('\\') {
179 return format!("invalid skill name: {name}");
180 }
181 let skill_dir = self.skills.skills_dir.join(name);
182 let skill_file = skill_dir.join("SKILL.md");
183 let content = match tokio::fs::read_to_string(&skill_file).await {
184 Ok(c) => c,
185 Err(_) => return format!("skill not found: {name}"),
186 };
187 let skill = match loader::parse_skill_md(&content) {
188 Ok(s) => s,
189 Err(e) => return format!("failed to parse skill: {e}"),
190 };
191 let body = skill.body.clone();
192 self.skills.registry.lock().await.add(skill);
193 let dir_path = skill_dir.display();
194 format!("{body}\n\nSkill directory: {dir_path}")
195 }
196}
197
198impl Hook for DaemonHook {
199 fn on_build_agent(&self, config: AgentConfig) -> AgentConfig {
200 self.memory.on_build_agent(config)
201 }
202
203 async fn on_register_tools(&self, tools: &mut ToolRegistry) {
204 self.memory.on_register_tools(tools).await;
205 self.mcp.on_register_tools(tools).await;
206 self.os.on_register_tools(tools).await;
207 self.register_system_tools(tools);
208 }
209
210 fn on_event(&self, agent: &str, event: &AgentEvent) {
211 match event {
212 AgentEvent::TextDelta(text) => {
213 tracing::trace!(%agent, text_len = text.len(), "agent text delta");
214 }
215 AgentEvent::ToolCallsStart(calls) => {
216 tracing::debug!(%agent, count = calls.len(), "agent tool calls started");
217 }
218 AgentEvent::ToolResult { call_id, .. } => {
219 tracing::debug!(%agent, %call_id, "agent tool result");
220 }
221 AgentEvent::ToolCallsComplete => {
222 tracing::debug!(%agent, "agent tool calls complete");
223 }
224 AgentEvent::Done(response) => {
225 tracing::info!(
226 %agent,
227 iterations = response.iterations,
228 stop_reason = ?response.stop_reason,
229 "agent run complete"
230 );
231 }
232 }
233 }
234}
235
236impl DaemonHook {
237 fn register_system_tools(&self, tools: &mut ToolRegistry) {
239 tools.insert(Tool {
240 name: "search_mcp".into(),
241 description: "Search available MCP tools by keyword.".into(),
242 parameters: schemars::schema_for!(SearchMcpInput),
243 strict: false,
244 });
245 tools.insert(Tool {
246 name: "call_mcp_tool".into(),
247 description: "Call an MCP tool by name with JSON-encoded arguments.".into(),
248 parameters: schemars::schema_for!(CallMcpToolInput),
249 strict: false,
250 });
251 tools.insert(Tool {
252 name: "search_skill".into(),
253 description: "Search available skills by keyword. Returns name and description only."
254 .into(),
255 parameters: schemars::schema_for!(SearchSkillInput),
256 strict: false,
257 });
258 tools.insert(Tool {
259 name: "load_skill".into(),
260 description: "Load a skill by name. Returns its instructions and the skill directory path for resolving relative file references.".into(),
261 parameters: schemars::schema_for!(LoadSkillInput),
262 strict: false,
263 });
264 }
265}