1pub mod agent_setup;
2use tracing::{debug, instrument, warn};
10
11mod browser;
12mod cron_tool;
13mod devices;
14pub mod exo_ai;
15mod file;
16mod gateway_tools;
17mod helpers;
18mod memory_tools;
19pub mod npm;
20pub mod ollama;
21mod patch;
22mod runtime;
23mod secrets_tools;
24mod sessions_tools;
25mod skills_tools;
26mod sysadmin;
27mod system_tools;
28pub mod uv;
29mod web;
30mod pdf;
31use uv::exec_uv_manage;
33
34use npm::exec_npm_manage;
36
37use agent_setup::exec_agent_setup;
39mod params;
40
41pub use helpers::{
43 SharedVault, VAULT_ACCESS_DENIED, command_references_credentials, expand_tilde, init_sandbox,
44 is_protected_path, process_manager, run_sandboxed_command, sandbox, sanitize_tool_output,
45 set_credentials_dir, set_vault, vault,
46};
47
48use file::{
50 exec_edit_file, exec_find_files, exec_list_directory, exec_read_file, exec_search_files,
51 exec_write_file,
52};
53
54use runtime::{exec_execute_command, exec_process};
56
57use web::{exec_web_fetch, exec_web_search};
59
60use memory_tools::{exec_memory_get, exec_memory_search, exec_save_memory, exec_search_history};
62
63use cron_tool::exec_cron;
65
66use sessions_tools::{
68 exec_agents_list, exec_session_status, exec_sessions_history, exec_sessions_list,
69 exec_sessions_send, exec_sessions_spawn,
70};
71
72use patch::exec_apply_patch;
74
75use gateway_tools::{exec_gateway, exec_image, exec_message, exec_tts};
77
78use devices::{exec_canvas, exec_nodes};
80
81use browser::exec_browser;
83
84use skills_tools::{
86 exec_skill_create, exec_skill_enable, exec_skill_info, exec_skill_install,
87 exec_skill_link_secret, exec_skill_list, exec_skill_search,
88};
89
90mod mcp_tools;
92use mcp_tools::{exec_mcp_connect, exec_mcp_disconnect, exec_mcp_list};
93
94mod task_tools;
96use task_tools::{
97 exec_task_background, exec_task_cancel, exec_task_describe, exec_task_foreground,
98 exec_task_input, exec_task_list, exec_task_pause, exec_task_resume, exec_task_status,
99};
100
101mod model_tools;
103use model_tools::{
104 exec_model_disable, exec_model_enable, exec_model_list, exec_model_recommend, exec_model_set,
105};
106
107use secrets_tools::exec_secrets_stub;
109
110use system_tools::{
112 exec_app_index, exec_audit_sensitive, exec_battery_health, exec_browser_cache,
113 exec_classify_files, exec_clipboard, exec_cloud_browse, exec_disk_usage, exec_screenshot,
114 exec_secure_delete, exec_summarize_file, exec_system_monitor,
115};
116
117use sysadmin::{
119 exec_firewall, exec_net_info, exec_net_scan, exec_pkg_manage, exec_service_manage,
120 exec_user_manage,
121};
122
123use pdf::exec_pdf;
125
126use exo_ai::exec_exo_manage;
128
129use ollama::exec_ollama_manage;
131
132fn exec_ask_user_stub(_args: &Value, _workspace_dir: &Path) -> Result<String, String> {
136 Err("ask_user must be executed via the gateway".into())
137}
138
139use serde::{Deserialize, Serialize};
140use serde_json::{Value, json};
141use std::path::Path;
142
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
147#[serde(rename_all = "snake_case")]
148pub enum ToolPermission {
149 Allow,
151 Deny,
153 Ask,
155 SkillOnly(Vec<String>),
157}
158
159impl Default for ToolPermission {
160 fn default() -> Self {
161 Self::Allow
162 }
163}
164
165impl std::fmt::Display for ToolPermission {
166 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167 match self {
168 Self::Allow => write!(f, "Allow"),
169 Self::Deny => write!(f, "Deny"),
170 Self::Ask => write!(f, "Ask"),
171 Self::SkillOnly(skills) => {
172 if skills.is_empty() {
173 write!(f, "Skill Only (none)")
174 } else {
175 write!(f, "Skill Only ({})", skills.join(", "))
176 }
177 }
178 }
179 }
180}
181
182impl ToolPermission {
183 pub fn cycle(&self) -> Self {
186 match self {
187 Self::Allow => Self::Ask,
188 Self::Ask => Self::Deny,
189 Self::Deny => Self::SkillOnly(Vec::new()),
190 Self::SkillOnly(_) => Self::Allow,
191 }
192 }
193
194 pub fn badge(&self) -> &'static str {
196 match self {
197 Self::Allow => "ALLOW",
198 Self::Deny => "DENY",
199 Self::Ask => "ASK",
200 Self::SkillOnly(_) => "SKILL",
201 }
202 }
203
204 pub fn description(&self) -> &'static str {
206 match self {
207 Self::Allow => "Tool runs automatically — no confirmation needed.",
208 Self::Deny => "Tool is blocked — the model receives an error and cannot use it.",
209 Self::Ask => "You will be prompted to approve or deny each use of this tool.",
210 Self::SkillOnly(_) => "Tool can only be used by named skills, not in direct chat.",
211 }
212 }
213}
214
215pub fn all_tool_names() -> Vec<&'static str> {
217 let mut names: Vec<&'static str> = all_tools().iter().map(|t| t.name).collect();
218 names.sort();
219 names
220}
221
222pub fn tool_summary(name: &str) -> &'static str {
224 match name {
225 "read_file" => "Read files on your computer",
226 "write_file" => "Create or overwrite files",
227 "edit_file" => "Edit existing files",
228 "list_directory" => "List folder contents",
229 "search_files" => "Search inside file contents",
230 "find_files" => "Find files by name",
231 "execute_command" => "Run shell commands",
232 "web_fetch" => "Fetch content from URLs",
233 "web_search" => "Search the web",
234 "process" => "Manage background processes",
235 "memory_search" => "Search agent memory files",
236 "memory_get" => "Read agent memory files",
237 "save_memory" => "Save memories (two-layer consolidation)",
238 "search_history" => "Search HISTORY.md for past entries",
239 "cron" => "Manage scheduled jobs",
240 "sessions_list" => "List active sessions",
241 "sessions_spawn" => "Spawn async sub-agents (use cheaper models for simple tasks)",
242 "sessions_send" => "Send messages to sessions",
243 "sessions_history" => "Read session message history",
244 "session_status" => "Check session status & usage",
245 "agents_list" => "List available agent types",
246 "apply_patch" => "Apply diff patches to files",
247 "secrets_list" => "List vault secret names",
248 "secrets_get" => "Read secrets from the vault",
249 "secrets_store" => "Store secrets in the vault",
250 "secrets_set_policy" => "Change credential access policy",
251 "gateway" => "Control the gateway daemon",
252 "message" => "Send messages via channels",
253 "tts" => "Convert text to speech",
254 "image" => "Analyze images with vision AI",
255 "nodes" => "Control paired companion devices",
256 "browser" => "Automate a web browser",
257 "canvas" => "Display UI on node canvases",
258 "skill_list" => "List loaded skills",
259 "skill_search" => "Search the skill registry",
260 "skill_install" => "Install skills from registry",
261 "skill_info" => "View skill details",
262 "skill_enable" => "Enable or disable skills",
263 "skill_link_secret" => "Link vault secrets to skills",
264 "skill_create" => "Create a new skill from scratch",
265 "mcp_list" => "List connected MCP servers",
266 "mcp_connect" => "Connect to an MCP server",
267 "mcp_disconnect" => "Disconnect from an MCP server",
268 "task_list" => "List active tasks",
269 "task_status" => "Get task status by ID",
270 "task_foreground" => "Bring task to foreground",
271 "task_background" => "Move task to background",
272 "task_cancel" => "Cancel a running task",
273 "task_pause" => "Pause a running task",
274 "task_resume" => "Resume a paused task",
275 "task_input" => "Send input to a task",
276 "task_describe" => "Set task description (shown in sidebar)",
277 "thread_describe" => "Set conversation thread description (shown in sidebar)",
278 "model_list" => "List available models with cost tiers",
279 "model_enable" => "Enable a model for use",
280 "model_disable" => "Disable a model",
281 "model_set" => "Set the active model",
282 "model_recommend" => "Get model recommendation for task complexity",
283 "disk_usage" => "Scan disk usage by folder",
284 "classify_files" => "Categorize files as docs, caches, etc.",
285 "system_monitor" => "View CPU, memory & process info",
286 "battery_health" => "Check battery status & health",
287 "app_index" => "List installed apps by size",
288 "cloud_browse" => "Browse local cloud storage folders",
289 "browser_cache" => "Audit or clean browser caches",
290 "screenshot" => "Capture a screenshot",
291 "clipboard" => "Read or write the clipboard",
292 "audit_sensitive" => "Scan files for exposed secrets",
293 "secure_delete" => "Securely overwrite & delete files",
294 "summarize_file" => "Preview-summarize any file type",
295 "ask_user" => "Ask the user structured questions",
296 "ollama_manage" => "Administer the Ollama model server",
297 "exo_manage" => "Administer the Exo distributed AI cluster (git clone + uv run)",
298 "uv_manage" => "Manage Python envs & packages via uv",
299 "npm_manage" => "Manage Node.js packages & scripts via npm",
300 "agent_setup" => "Set up local model infrastructure",
301 "pdf" => "Analyze PDF files (extract text, metadata, page counts)",
302 _ => "Unknown tool",
303 }
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize)]
310pub struct ToolParam {
311 pub name: String,
312 pub description: String,
313 #[serde(rename = "type")]
315 pub param_type: String,
316 pub required: bool,
317}
318
319pub type SyncExecuteFn = fn(args: &Value, workspace_dir: &Path) -> Result<String, String>;
321
322#[derive(Clone)]
324pub struct ToolDef {
325 pub name: &'static str,
326 pub description: &'static str,
327 pub parameters: Vec<ToolParam>,
328 pub execute: SyncExecuteFn,
331}
332
333impl std::fmt::Debug for ToolDef {
334 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
335 f.debug_struct("ToolDef")
336 .field("name", &self.name)
337 .field("description", &self.description)
338 .field("parameters", &self.parameters)
339 .finish()
340 }
341}
342
343pub fn all_tools() -> Vec<&'static ToolDef> {
347 vec![
348 &READ_FILE,
349 &WRITE_FILE,
350 &EDIT_FILE,
351 &LIST_DIRECTORY,
352 &SEARCH_FILES,
353 &FIND_FILES,
354 &EXECUTE_COMMAND,
355 &WEB_FETCH,
356 &WEB_SEARCH,
357 &PROCESS,
358 &MEMORY_SEARCH,
359 &MEMORY_GET,
360 &SAVE_MEMORY,
361 &SEARCH_HISTORY,
362 &CRON,
363 &SESSIONS_LIST,
364 &SESSIONS_SPAWN,
365 &SESSIONS_SEND,
366 &SESSIONS_HISTORY,
367 &SESSION_STATUS,
368 &AGENTS_LIST,
369 &APPLY_PATCH,
370 &SECRETS_LIST,
371 &SECRETS_GET,
372 &SECRETS_STORE,
373 &SECRETS_SET_POLICY,
374 &GATEWAY,
375 &MESSAGE,
376 &TTS,
377 &IMAGE,
378 &NODES,
379 &BROWSER,
380 &CANVAS,
381 &SKILL_LIST,
382 &SKILL_SEARCH,
383 &SKILL_INSTALL,
384 &SKILL_INFO,
385 &SKILL_ENABLE,
386 &SKILL_LINK_SECRET,
387 &SKILL_CREATE,
388 &MCP_LIST,
389 &MCP_CONNECT,
390 &MCP_DISCONNECT,
391 &TASK_LIST,
392 &TASK_STATUS,
393 &TASK_FOREGROUND,
394 &TASK_BACKGROUND,
395 &TASK_CANCEL,
396 &TASK_PAUSE,
397 &TASK_RESUME,
398 &TASK_INPUT,
399 &TASK_DESCRIBE,
400 &THREAD_DESCRIBE,
401 &MODEL_LIST,
402 &MODEL_ENABLE,
403 &MODEL_DISABLE,
404 &MODEL_SET,
405 &MODEL_RECOMMEND,
406 &DISK_USAGE,
407 &CLASSIFY_FILES,
408 &SYSTEM_MONITOR,
409 &BATTERY_HEALTH,
410 &APP_INDEX,
411 &CLOUD_BROWSE,
412 &BROWSER_CACHE,
413 &SCREENSHOT,
414 &CLIPBOARD,
415 &AUDIT_SENSITIVE,
416 &SECURE_DELETE,
417 &SUMMARIZE_FILE,
418 &PKG_MANAGE,
419 &NET_INFO,
420 &NET_SCAN,
421 &SERVICE_MANAGE,
422 &USER_MANAGE,
423 &FIREWALL,
424 &OLLAMA_MANAGE,
425 &EXO_MANAGE,
426 &UV_MANAGE,
427 &NPM_MANAGE,
428 &AGENT_SETUP,
429 &ASK_USER,
430 &PDF,
431 ]
432}
433
434pub static READ_FILE: ToolDef = ToolDef {
438 name: "read_file",
439 description: "Read the contents of a file. Returns the file text. \
440 Handles plain text files directly and can also extract \
441 text from .docx, .doc, .rtf, .odt, .pdf, and .html files. \
442 If you have an absolute path from find_files or search_files, \
443 pass it exactly as-is. Use the optional start_line / end_line \
444 parameters to read a specific range (1-based, inclusive).",
445 parameters: vec![], execute: exec_read_file,
447};
448
449pub static WRITE_FILE: ToolDef = ToolDef {
450 name: "write_file",
451 description: "Create or overwrite a file with the given content. \
452 Parent directories are created automatically.",
453 parameters: vec![],
454 execute: exec_write_file,
455};
456
457pub static EDIT_FILE: ToolDef = ToolDef {
458 name: "edit_file",
459 description: "Make a targeted edit to an existing file using search-and-replace. \
460 The old_string must match exactly one location in the file. \
461 Include enough context lines to make the match unique.",
462 parameters: vec![],
463 execute: exec_edit_file,
464};
465
466pub static LIST_DIRECTORY: ToolDef = ToolDef {
467 name: "list_directory",
468 description: "List the contents of a directory. Returns file and \
469 directory names, with directories suffixed by '/'.",
470 parameters: vec![],
471 execute: exec_list_directory,
472};
473
474pub static SEARCH_FILES: ToolDef = ToolDef {
475 name: "search_files",
476 description: "Search file CONTENTS for a text pattern (like grep -i). \
477 The search is case-insensitive. Returns matching lines \
478 with paths and line numbers. Use `find_files` instead \
479 when searching by file name. Set `path` to an absolute \
480 directory (e.g. '/Users/alice') to search outside the \
481 workspace.",
482 parameters: vec![],
483 execute: exec_search_files,
484};
485
486pub static FIND_FILES: ToolDef = ToolDef {
487 name: "find_files",
488 description: "Find files by name. Returns paths that can be passed directly to read_file. Accepts plain keywords (case-insensitive \
489 substring match) OR glob patterns (e.g. '*.pdf'). Multiple \
490 keywords can be separated with spaces — a file matches if its \
491 name contains ANY keyword. Examples: 'resume', 'resume cv', \
492 '*.pdf'. Set `path` to an absolute directory to search outside \
493 the workspace (e.g. '/Users/alice'). Use `search_files` to \
494 search file CONTENTS instead.",
495 parameters: vec![],
496 execute: exec_find_files,
497};
498
499pub static EXECUTE_COMMAND: ToolDef = ToolDef {
500 name: "execute_command",
501 description: "Execute a shell command and return output (stdout + stderr). \
502 Runs via `sh -c` in the workspace directory.\n\n\
503 **Common uses:**\n\
504 - Git: git status, git commit, git push\n\
505 - Build: cargo build, npm install, make\n\
506 - System: find, grep, curl, ssh\n\
507 - API calls: curl -H 'Authorization: token ...' URL\n\n\
508 For long-running commands, use background=true and poll with process tool. \
509 Set working_dir for different directory.",
510 parameters: vec![],
511 execute: exec_execute_command,
512};
513
514pub static WEB_FETCH: ToolDef = ToolDef {
515 name: "web_fetch",
516 description: "Fetch and extract readable content from a URL (HTML → markdown or plain text). \
517 Use for reading web pages, documentation, articles, APIs, or any HTTP content.\n\n\
518 **For API calls:** Use the 'authorization' parameter to pass auth headers:\n\
519 - GitHub: authorization='token ghp_...'\n\
520 - Bearer tokens: authorization='Bearer eyJ...'\n\
521 - API keys: Use 'headers' param: {\"X-Api-Key\": \"...\"}\n\n\
522 Set use_cookies=true for sites requiring login cookies. \
523 For JavaScript-heavy sites, use browser tools instead.",
524 parameters: vec![],
525 execute: exec_web_fetch,
526};
527
528pub static WEB_SEARCH: ToolDef = ToolDef {
529 name: "web_search",
530 description: "Search the web using Brave Search API. Returns titles, URLs, and snippets. \
531 Requires BRAVE_API_KEY environment variable to be set. \
532 Use for finding current information, research, and fact-checking.",
533 parameters: vec![],
534 execute: exec_web_search,
535};
536
537pub static PROCESS: ToolDef = ToolDef {
538 name: "process",
539 description: "Manage background exec sessions. Actions: list (show all sessions), \
540 poll (get new output + status for a session), log (get output with offset/limit), \
541 write (send data to stdin), kill (terminate a session), clear (remove completed sessions), \
542 remove (remove a specific session).",
543 parameters: vec![],
544 execute: exec_process,
545};
546
547pub static MEMORY_SEARCH: ToolDef = ToolDef {
548 name: "memory_search",
549 description: "Semantically search MEMORY.md and memory/*.md files for relevant information. \
550 Use before answering questions about prior work, decisions, dates, people, \
551 preferences, or todos. Returns matching snippets with file path and line numbers.",
552 parameters: vec![],
553 execute: exec_memory_search,
554};
555
556pub static MEMORY_GET: ToolDef = ToolDef {
557 name: "memory_get",
558 description: "Read content from a memory file (MEMORY.md or memory/*.md). \
559 Use after memory_search to get full context around a snippet. \
560 Supports optional line range for large files.",
561 parameters: vec![],
562 execute: exec_memory_get,
563};
564
565pub static SAVE_MEMORY: ToolDef = ToolDef {
566 name: "save_memory",
567 description: "Save memories using two-layer consolidation. Appends a timestamped entry to HISTORY.md \
568 (grep-searchable log) and optionally updates MEMORY.md (curated long-term facts). \
569 Use to persist important context, decisions, and facts for future recall.",
570 parameters: vec![],
571 execute: exec_save_memory,
572};
573
574pub static SEARCH_HISTORY: ToolDef = ToolDef {
575 name: "search_history",
576 description: "Search HISTORY.md for past entries matching a pattern. Returns timestamped entries \
577 that match the query. Use to recall when something happened or find past events.",
578 parameters: vec![],
579 execute: exec_search_history,
580};
581
582pub static CRON: ToolDef = ToolDef {
583 name: "cron",
584 description: "Manage scheduled jobs. Actions: status (scheduler status), list (show jobs), \
585 add (create job), update (modify job), remove (delete job), run (trigger immediately), \
586 runs (get run history). Use for reminders and recurring tasks.",
587 parameters: vec![],
588 execute: exec_cron,
589};
590
591pub static SESSIONS_LIST: ToolDef = ToolDef {
592 name: "sessions_list",
593 description: "List active sessions with optional filters. Shows main sessions and sub-agents. \
594 Use to check on running background tasks.",
595 parameters: vec![],
596 execute: exec_sessions_list,
597};
598
599pub static SESSIONS_SPAWN: ToolDef = ToolDef {
600 name: "sessions_spawn",
601 description: "Spawn a sub-agent to run a task asynchronously. Sub-agents run in isolated sessions \
602 and announce results when finished. SPAWN FREELY — the system handles concurrency efficiently.\n\n\
603 **Model selection guidance:**\n\
604 - Use `model_recommend` to get a cost-appropriate model for the task\n\
605 - Simple tasks (grep, format, list) → use free/economy models (llama3.2, claude-haiku)\n\
606 - Medium tasks (code edits, analysis) → use economy/standard models\n\
607 - Complex tasks (debugging, architecture) → use standard models\n\
608 - Critical tasks (security, production) → use premium models\n\n\
609 Multiple sub-agents can run concurrently. Continue working while they run.",
610 parameters: vec![],
611 execute: exec_sessions_spawn,
612};
613
614pub static SESSIONS_SEND: ToolDef = ToolDef {
615 name: "sessions_send",
616 description: "Send a message to another session. Use sessionKey or label to identify the target. \
617 Returns immediately after sending.",
618 parameters: vec![],
619 execute: exec_sessions_send,
620};
621
622pub static SESSIONS_HISTORY: ToolDef = ToolDef {
623 name: "sessions_history",
624 description: "Fetch message history for a session. Returns recent messages from the specified session.",
625 parameters: vec![],
626 execute: exec_sessions_history,
627};
628
629pub static SESSION_STATUS: ToolDef = ToolDef {
630 name: "session_status",
631 description: "Show session status including usage, time, and cost. Use for model-use questions. \
632 Can also set per-session model override.",
633 parameters: vec![],
634 execute: exec_session_status,
635};
636
637pub static AGENTS_LIST: ToolDef = ToolDef {
638 name: "agents_list",
639 description: "List available agent IDs that can be targeted with sessions_spawn. \
640 Returns the configured agents based on allowlists.",
641 parameters: vec![],
642 execute: exec_agents_list,
643};
644
645pub static APPLY_PATCH: ToolDef = ToolDef {
646 name: "apply_patch",
647 description: "Apply a unified diff patch to one or more files. Supports multi-hunk patches. \
648 Use for complex multi-line edits where edit_file would be cumbersome.",
649 parameters: vec![],
650 execute: exec_apply_patch,
651};
652
653pub static SECRETS_LIST: ToolDef = ToolDef {
654 name: "secrets_list",
655 description: "**CHECK THIS FIRST** before asking the user for API keys or tokens! \
656 Lists all credentials stored in the encrypted vault with their names, types, and access policies. \
657 If a credential exists here, use secrets_get to retrieve it — don't ask the user for it again.",
658 parameters: vec![],
659 execute: exec_secrets_stub,
660};
661
662pub static SECRETS_GET: ToolDef = ToolDef {
663 name: "secrets_get",
664 description: "Retrieve a credential from the vault by name. Returns the value directly.\n\n\
665 **Common workflow:**\n\
666 1. secrets_list() → see available credentials\n\
667 2. secrets_get(name='github_token') → get the token value\n\
668 3. web_fetch(url='...', authorization='token <value>') → use it\n\n\
669 For HTTP APIs, pass the token to web_fetch via 'authorization' parameter. \
670 For CLI tools, use execute_command with the token in headers or env vars.",
671 parameters: vec![],
672 execute: exec_secrets_stub,
673};
674
675pub static SECRETS_STORE: ToolDef = ToolDef {
676 name: "secrets_store",
677 description: "Store or update a credential in the encrypted secrets vault. \
678 The value is encrypted at rest. Use for API keys, tokens, and \
679 other sensitive material. Set policy to 'always' for agent access.",
680 parameters: vec![],
681 execute: exec_secrets_stub,
682};
683
684pub static SECRETS_SET_POLICY: ToolDef = ToolDef {
685 name: "secrets_set_policy",
686 description: "Change the access policy of an existing credential. Policies: \
687 always (agent can read freely), approval (requires user approval), \
688 auth (requires re-authentication), skill:<name> (only named skill).",
689 parameters: vec![],
690 execute: exec_secrets_stub,
691};
692
693pub static GATEWAY: ToolDef = ToolDef {
694 name: "gateway",
695 description: "Manage the gateway daemon. Actions: restart (restart gateway), \
696 config.get (get current config), config.schema (get config schema), \
697 config.apply (replace entire config), config.patch (partial config update), \
698 update.run (update gateway).",
699 parameters: vec![],
700 execute: exec_gateway,
701};
702
703pub static MESSAGE: ToolDef = ToolDef {
704 name: "message",
705 description: "Send messages via configured channels (telegram, discord, whatsapp, signal, matrix, etc.).\n\n\
706 **Actions:** send, poll, react, thread-create, thread-reply, search, pin, edit, delete\n\n\
707 **Example:** message(action='send', channel='telegram', target='@username', message='Hello')\n\n\
708 Use for proactive notifications, cross-channel messaging, or channel-specific features \
709 like reactions, threads, and polls. The channel parameter selects which messenger to use.",
710 parameters: vec![],
711 execute: exec_message,
712};
713
714pub static TTS: ToolDef = ToolDef {
715 name: "tts",
716 description: "Convert text to speech and return a media path. Use when the user \
717 requests audio or TTS is enabled.",
718 parameters: vec![],
719 execute: exec_tts,
720};
721
722pub static IMAGE: ToolDef = ToolDef {
723 name: "image",
724 description: "Analyze an image using the configured image/vision model. \
725 Pass a local file path or URL. Returns a text description or \
726 answers the prompt about the image.",
727 parameters: vec![],
728 execute: exec_image,
729};
730
731pub static NODES: ToolDef = ToolDef {
732 name: "nodes",
733 description: "Discover and control paired nodes (companion devices). Actions: \
734 status (list nodes), describe (node details), pending/approve/reject (pairing), \
735 notify (send notification), camera_snap/camera_list (camera), \
736 screen_record (screen capture), location_get (GPS), run/invoke (remote commands).",
737 parameters: vec![],
738 execute: exec_nodes,
739};
740
741pub static BROWSER: ToolDef = ToolDef {
742 name: "browser",
743 description: "Control web browser for automation. Actions: status, start, stop, \
744 profiles, tabs, open, focus, close, snapshot, screenshot, navigate, \
745 console, pdf, act (click/type/press/hover/drag). Use snapshot to get \
746 page accessibility tree for element targeting.",
747 parameters: vec![],
748 execute: exec_browser,
749};
750
751pub static CANVAS: ToolDef = ToolDef {
752 name: "canvas",
753 description: "Control node canvases for UI presentation. Actions: present (show content), \
754 hide, navigate, eval (run JavaScript), snapshot (capture rendered UI), \
755 a2ui_push/a2ui_reset (accessibility-to-UI).",
756 parameters: vec![],
757 execute: exec_canvas,
758};
759
760pub static SKILL_LIST: ToolDef = ToolDef {
761 name: "skill_list",
762 description: "List all loaded skills with their status (enabled, gates, source, linked secrets). \
763 Use to discover what capabilities are available.",
764 parameters: vec![],
765 execute: exec_skill_list,
766};
767
768pub static SKILL_SEARCH: ToolDef = ToolDef {
769 name: "skill_search",
770 description: "Search the ClawHub registry for installable skills. Returns skill names, \
771 descriptions, versions, and required secrets.",
772 parameters: vec![],
773 execute: exec_skill_search,
774};
775
776pub static SKILL_INSTALL: ToolDef = ToolDef {
777 name: "skill_install",
778 description: "Install a skill from the ClawHub registry by name. Optionally specify a version. \
779 After installation the skill is immediately available. Use skill_link_secret to \
780 bind required credentials.",
781 parameters: vec![],
782 execute: exec_skill_install,
783};
784
785pub static SKILL_INFO: ToolDef = ToolDef {
786 name: "skill_info",
787 description: "Show detailed information about a loaded skill: description, source, linked \
788 secrets, gating status, and instructions summary.",
789 parameters: vec![],
790 execute: exec_skill_info,
791};
792
793pub static SKILL_ENABLE: ToolDef = ToolDef {
794 name: "skill_enable",
795 description: "Enable or disable a loaded skill. Disabled skills are not injected into the \
796 agent prompt and cannot be activated.",
797 parameters: vec![],
798 execute: exec_skill_enable,
799};
800
801pub static SKILL_LINK_SECRET: ToolDef = ToolDef {
802 name: "skill_link_secret",
803 description: "Link or unlink a vault credential to a skill. When linked, the secret is \
804 accessible under the SkillOnly policy while the skill is active. Use action \
805 'link' to bind or 'unlink' to remove the binding.",
806 parameters: vec![],
807 execute: exec_skill_link_secret,
808};
809
810pub static SKILL_CREATE: ToolDef = ToolDef {
811 name: "skill_create",
812 description: "Create a new skill on disk. Provide a name (kebab-case), a one-line \
813 description, and the full markdown instructions body. The skill directory \
814 and SKILL.md file are created automatically and the skill is immediately \
815 available for use.",
816 parameters: vec![],
817 execute: exec_skill_create,
818};
819
820pub static MCP_LIST: ToolDef = ToolDef {
823 name: "mcp_list",
824 description: "List connected MCP (Model Context Protocol) servers and their available tools. \
825 Shows server name, connection status, and tool count.",
826 parameters: vec![],
827 execute: exec_mcp_list,
828};
829
830pub static MCP_CONNECT: ToolDef = ToolDef {
831 name: "mcp_connect",
832 description: "Connect to an MCP server by name (from config) or command. \
833 Parameters: name (string, server name from config), or command (string) + args (array).",
834 parameters: vec![],
835 execute: exec_mcp_connect,
836};
837
838pub static MCP_DISCONNECT: ToolDef = ToolDef {
839 name: "mcp_disconnect",
840 description: "Disconnect from an MCP server by name.",
841 parameters: vec![],
842 execute: exec_mcp_disconnect,
843};
844
845pub static TASK_LIST: ToolDef = ToolDef {
848 name: "task_list",
849 description: "List active tasks. Tasks include running commands, sub-agents, cron jobs, \
850 and other long-running operations. Shows task ID, kind, status, and progress.",
851 parameters: vec![],
852 execute: exec_task_list,
853};
854
855pub static TASK_STATUS: ToolDef = ToolDef {
856 name: "task_status",
857 description: "Get detailed status of a specific task by ID.",
858 parameters: vec![],
859 execute: exec_task_status,
860};
861
862pub static TASK_FOREGROUND: ToolDef = ToolDef {
863 name: "task_foreground",
864 description: "Bring a task to the foreground. Foreground tasks stream their output \
865 to the user in real-time. Only one task per session can be foregrounded.",
866 parameters: vec![],
867 execute: exec_task_foreground,
868};
869
870pub static TASK_BACKGROUND: ToolDef = ToolDef {
871 name: "task_background",
872 description: "Move a task to the background. Background tasks continue running but \
873 don't stream output. Their output is buffered for later review.",
874 parameters: vec![],
875 execute: exec_task_background,
876};
877
878pub static TASK_CANCEL: ToolDef = ToolDef {
879 name: "task_cancel",
880 description: "Cancel a running task. The task will be terminated and marked as cancelled.",
881 parameters: vec![],
882 execute: exec_task_cancel,
883};
884
885pub static TASK_PAUSE: ToolDef = ToolDef {
886 name: "task_pause",
887 description: "Pause a running task. Not all task types support pausing.",
888 parameters: vec![],
889 execute: exec_task_pause,
890};
891
892pub static TASK_RESUME: ToolDef = ToolDef {
893 name: "task_resume",
894 description: "Resume a paused task.",
895 parameters: vec![],
896 execute: exec_task_resume,
897};
898
899pub static TASK_INPUT: ToolDef = ToolDef {
900 name: "task_input",
901 description: "Send input to a task that is waiting for user input.",
902 parameters: vec![],
903 execute: exec_task_input,
904};
905
906pub static TASK_DESCRIBE: ToolDef = ToolDef {
907 name: "task_describe",
908 description: "Set a short description of what the task is currently doing. \
909 This description is displayed in the sidebar. \
910 If no task ID is provided, sets description for the current task.",
911 parameters: vec![],
912 execute: exec_task_describe,
913};
914
915pub const THREAD_UPDATE_MARKER: &str = "🏷️THREAD_UPDATE:";
919
920pub static THREAD_DESCRIBE: ToolDef = ToolDef {
921 name: "thread_describe",
922 description: "Set a description for the current conversation thread. \
923 This description is displayed in the sidebar and helps track what the thread is about. \
924 Call this when starting a new task or when the thread's focus changes significantly.",
925 parameters: vec![],
926 execute: exec_thread_describe,
927};
928
929fn exec_thread_describe(args: &Value, _workspace_dir: &Path) -> Result<String, String> {
930 let description = args
931 .get("description")
932 .and_then(|v| v.as_str())
933 .ok_or("Missing required parameter: description")?;
934
935 let update = json!({
937 "action": "set_description",
938 "description": description,
939 });
940
941 Ok(format!("{}{}", THREAD_UPDATE_MARKER, update))
942}
943
944pub static MODEL_LIST: ToolDef = ToolDef {
947 name: "model_list",
948 description: "List available models with their cost tiers and status. \
949 Models are categorized as: 🆓 Free, 💰 Economy, ⚖️ Standard, 💎 Premium. \
950 Use tier parameter to filter. Shows enabled/disabled and available status.",
951 parameters: vec![],
952 execute: exec_model_list,
953};
954
955pub static MODEL_ENABLE: ToolDef = ToolDef {
956 name: "model_enable",
957 description: "Enable a model for use. Enabling a model makes it available for selection \
958 as the active model or for sub-agent use.",
959 parameters: vec![],
960 execute: exec_model_enable,
961};
962
963pub static MODEL_DISABLE: ToolDef = ToolDef {
964 name: "model_disable",
965 description: "Disable a model. Disabled models won't be used even if credentials are available.",
966 parameters: vec![],
967 execute: exec_model_disable,
968};
969
970pub static MODEL_SET: ToolDef = ToolDef {
971 name: "model_set",
972 description: "Set the active model for this session. The active model handles all chat requests.",
973 parameters: vec![],
974 execute: exec_model_set,
975};
976
977pub static MODEL_RECOMMEND: ToolDef = ToolDef {
978 name: "model_recommend",
979 description: "Get a model recommendation for a given task complexity. \
980 Complexity levels: simple (use free/economy), medium (economy/standard), \
981 complex (standard), critical (premium). \
982 Use this when spawning sub-agents to pick cost-effective models.",
983 parameters: vec![],
984 execute: exec_model_recommend,
985};
986
987pub static DISK_USAGE: ToolDef = ToolDef {
990 name: "disk_usage",
991 description: "Scan disk usage for a directory tree. Returns the largest entries \
992 sorted by size. Defaults to the home directory. Use `depth` to \
993 control how deep to scan and `top` to limit results.",
994 parameters: vec![],
995 execute: exec_disk_usage,
996};
997
998pub static CLASSIFY_FILES: ToolDef = ToolDef {
999 name: "classify_files",
1000 description: "Classify files in a directory as user documents, caches, logs, \
1001 build artifacts, cloud storage, images, video, audio, archives, \
1002 installers, or app config. Useful for understanding what's in a folder.",
1003 parameters: vec![],
1004 execute: exec_classify_files,
1005};
1006
1007pub static SYSTEM_MONITOR: ToolDef = ToolDef {
1008 name: "system_monitor",
1009 description: "Return current system resource usage: CPU load, memory, disk space, \
1010 network, and top processes. Use `metric` to query a specific area \
1011 or 'all' for everything.",
1012 parameters: vec![],
1013 execute: exec_system_monitor,
1014};
1015
1016pub static BATTERY_HEALTH: ToolDef = ToolDef {
1017 name: "battery_health",
1018 description: "Report battery status including charge level, cycle count, capacity, \
1019 temperature, and charging state. Works on macOS and Linux laptops.",
1020 parameters: vec![],
1021 execute: exec_battery_health,
1022};
1023
1024pub static APP_INDEX: ToolDef = ToolDef {
1025 name: "app_index",
1026 description: "List installed applications with their size, version, and source \
1027 (native or Homebrew). Sort by size or name. Filter by substring.",
1028 parameters: vec![],
1029 execute: exec_app_index,
1030};
1031
1032pub static CLOUD_BROWSE: ToolDef = ToolDef {
1033 name: "cloud_browse",
1034 description: "Detect and browse local cloud storage sync folders (Google Drive, \
1035 Dropbox, OneDrive, iCloud). Use 'detect' to find them or 'list' \
1036 to browse files in a specific cloud folder.",
1037 parameters: vec![],
1038 execute: exec_cloud_browse,
1039};
1040
1041pub static BROWSER_CACHE: ToolDef = ToolDef {
1042 name: "browser_cache",
1043 description: "Audit or clean browser cache and download folders. Supports Chrome, \
1044 Firefox, Safari, Edge, and Arc. Use 'scan' to see sizes or 'clean' \
1045 to remove cache data.",
1046 parameters: vec![],
1047 execute: exec_browser_cache,
1048};
1049
1050pub static SCREENSHOT: ToolDef = ToolDef {
1051 name: "screenshot",
1052 description: "Capture a screenshot of the full screen or a specific region. \
1053 Supports optional delay. Saves as PNG. Uses screencapture on macOS \
1054 or imagemagick on Linux.",
1055 parameters: vec![],
1056 execute: exec_screenshot,
1057};
1058
1059pub static CLIPBOARD: ToolDef = ToolDef {
1060 name: "clipboard",
1061 description: "Read from or write to the system clipboard. Uses pbcopy/pbpaste \
1062 on macOS or xclip/xsel on Linux.",
1063 parameters: vec![],
1064 execute: exec_clipboard,
1065};
1066
1067pub static AUDIT_SENSITIVE: ToolDef = ToolDef {
1068 name: "audit_sensitive",
1069 description: "Scan source files for potentially sensitive data: AWS keys, private \
1070 keys, GitHub tokens, API keys, passwords, JWTs, Slack tokens. \
1071 Matches are redacted in output. Use for security reviews.",
1072 parameters: vec![],
1073 execute: exec_audit_sensitive,
1074};
1075
1076pub static SECURE_DELETE: ToolDef = ToolDef {
1077 name: "secure_delete",
1078 description: "Securely overwrite and delete a file or directory. Overwrites with \
1079 random data multiple passes before unlinking. Requires confirm=true \
1080 to proceed (first call returns file info for review). Refuses \
1081 critical system paths.",
1082 parameters: vec![],
1083 execute: exec_secure_delete,
1084};
1085
1086pub static SUMMARIZE_FILE: ToolDef = ToolDef {
1087 name: "summarize_file",
1088 description: "Generate a preview summary of any file: text files get head/tail and \
1089 definition extraction; PDFs get page count and text preview; images \
1090 get dimensions; media gets duration and codecs; archives get content \
1091 listing. Returns structured metadata.",
1092 parameters: vec![],
1093 execute: exec_summarize_file,
1094};
1095
1096pub static PKG_MANAGE: ToolDef = ToolDef {
1099 name: "pkg_manage",
1100 description: "Install, uninstall, upgrade, search, and list software packages. \
1101 Auto-detects the system package manager (brew, apt, dnf, pacman, \
1102 zypper, apk, snap, flatpak, port, nix-env) or use the manager \
1103 parameter to override. Also supports querying package info and \
1104 listing installed packages.",
1105 parameters: vec![],
1106 execute: exec_pkg_manage,
1107};
1108
1109pub static NET_INFO: ToolDef = ToolDef {
1110 name: "net_info",
1111 description: "Query network information: interfaces, active connections, routing \
1112 table, DNS lookups, ping, traceroute, whois, ARP table, public IP, \
1113 Wi-Fi details, and bandwidth statistics.",
1114 parameters: vec![],
1115 execute: exec_net_info,
1116};
1117
1118pub static NET_SCAN: ToolDef = ToolDef {
1119 name: "net_scan",
1120 description: "Network scanning and packet capture: run nmap scans (quick, full, \
1121 service, OS, UDP, vuln, ping, stealth), capture packets with tcpdump, \
1122 check if a specific port is open, listen for connections, sniff \
1123 traffic summaries, and discover hosts on the local network.",
1124 parameters: vec![],
1125 execute: exec_net_scan,
1126};
1127
1128pub static SERVICE_MANAGE: ToolDef = ToolDef {
1129 name: "service_manage",
1130 description: "Manage system services: list running/loaded services, check status, \
1131 start, stop, restart, enable, disable, and view logs. Auto-detects \
1132 the init system (systemd, launchd, sysvinit).",
1133 parameters: vec![],
1134 execute: exec_service_manage,
1135};
1136
1137pub static USER_MANAGE: ToolDef = ToolDef {
1138 name: "user_manage",
1139 description: "Manage system users and groups: whoami, list users, list groups, \
1140 get user info, add/remove users, add user to group, and view last \
1141 login history.",
1142 parameters: vec![],
1143 execute: exec_user_manage,
1144};
1145
1146pub static FIREWALL: ToolDef = ToolDef {
1147 name: "firewall",
1148 description: "Manage the system firewall: check status, list rules, allow or deny \
1149 a port (TCP/UDP), enable or disable the firewall. Auto-detects the \
1150 firewall backend (pf, ufw, firewalld, iptables, nftables).",
1151 parameters: vec![],
1152 execute: exec_firewall,
1153};
1154
1155pub static OLLAMA_MANAGE: ToolDef = ToolDef {
1158 name: "ollama_manage",
1159 description: "Administer the Ollama local model server. Actions: setup (install \
1160 ollama), serve/start, stop, status, pull/add (download a model), \
1161 rm/remove (delete a model), list (show downloaded models), \
1162 show/info (model details), ps/running (loaded models), \
1163 load/warm (preload into VRAM), unload/evict (free VRAM), \
1164 copy/cp (duplicate a model tag).",
1165 parameters: vec![],
1166 execute: exec_ollama_manage,
1167};
1168
1169pub static EXO_MANAGE: ToolDef = ToolDef {
1170 name: "exo_manage",
1171 description: "Administer the Exo distributed AI inference cluster (exo-explore/exo). \
1172 Actions: setup (clone repo, install prereqs, build dashboard), \
1173 start/run (launch exo node), stop, status (cluster overview with download \
1174 progress), models/list (available models), state/topology (cluster nodes, \
1175 instances & downloads), downloads/progress (show model download status with \
1176 progress bars), preview (placement previews for a model), load/add/pull \
1177 (create model instance / start download), unload/remove (delete instance), \
1178 update (git pull + rebuild), log (view logs).",
1179 parameters: vec![],
1180 execute: exec_exo_manage,
1181};
1182
1183pub static UV_MANAGE: ToolDef = ToolDef {
1184 name: "uv_manage",
1185 description: "Manage Python environments and packages via uv (ultra-fast package \
1186 manager). Actions: setup (install uv), version, venv (create virtualenv), \
1187 pip-install/add (install packages), pip-uninstall/remove (uninstall), \
1188 pip-list/list (show installed), pip-freeze/freeze (export requirements), \
1189 sync (install from requirements), run (execute in env), python \
1190 (install a Python version), init (create new project).",
1191 parameters: vec![],
1192 execute: exec_uv_manage,
1193};
1194
1195pub static NPM_MANAGE: ToolDef = ToolDef {
1196 name: "npm_manage",
1197 description: "Manage Node.js packages and scripts via npm. Actions: setup \
1198 (install Node.js/npm), version, init (create package.json), \
1199 npm-install/add (install packages), uninstall/remove, list, \
1200 outdated, update, run (run a script), start, build, test, \
1201 npx/exec (run a package binary), audit, cache-clean, info, \
1202 search, status.",
1203 parameters: vec![],
1204 execute: exec_npm_manage,
1205};
1206
1207pub static AGENT_SETUP: ToolDef = ToolDef {
1208 name: "agent_setup",
1209 description: "Set up the local model infrastructure in one command. Installs and \
1210 verifies uv (Python package manager), exo (distributed AI cluster), \
1211 and ollama (local model server). Use the optional 'components' \
1212 parameter to set up only specific tools (e.g. ['ollama','uv']).",
1213 parameters: vec![],
1214 execute: exec_agent_setup,
1215};
1216
1217pub static ASK_USER: ToolDef = ToolDef {
1220 name: "ask_user",
1221 description: "Ask the user a structured question. Opens an interactive dialog \
1222 in the TUI for the user to respond. Supports five prompt types: \
1223 'select' (pick one from a list), 'multi_select' (pick multiple), \
1224 'confirm' (yes/no), 'text' (free text input), and 'form' \
1225 (multiple named fields). Returns the user's answer as a JSON value. \
1226 Use this when you need specific, structured input rather than free chat.",
1227 parameters: vec![],
1228 execute: exec_ask_user_stub,
1229};
1230
1231pub static PDF: ToolDef = ToolDef {
1234 name: "pdf",
1235 description: "Analyze PDF files. Actions:\n\
1236 - extract: Extract text from a PDF (supports page ranges via start_page/end_page)\n\
1237 - info: Get PDF metadata (title, author, pages, etc.)\n\
1238 - page_count: Get the number of pages\n\n\
1239 Requires poppler-utils (pdftotext, pdfinfo) for best results. \
1240 Falls back to textutil (macOS) or pdfminer (Python).",
1241 parameters: vec![],
1242 execute: exec_pdf,
1243};
1244
1245pub use params::*;
1247
1248fn params_to_json_schema(params: &[ToolParam]) -> (Value, Value) {
1252 let mut properties = serde_json::Map::new();
1253 let mut required = Vec::new();
1254
1255 for p in params {
1256 let mut prop = serde_json::Map::new();
1257 prop.insert("type".into(), json!(p.param_type));
1258 prop.insert("description".into(), json!(p.description));
1259
1260 if p.param_type == "array" {
1262 prop.insert("items".into(), json!({"type": "string"}));
1263 }
1264
1265 properties.insert(p.name.clone(), Value::Object(prop));
1266 if p.required {
1267 required.push(json!(p.name));
1268 }
1269 }
1270
1271 (Value::Object(properties), Value::Array(required))
1272}
1273
1274fn resolve_params(tool: &ToolDef) -> Vec<ToolParam> {
1277 if !tool.parameters.is_empty() {
1278 return tool.parameters.clone();
1279 }
1280 match tool.name {
1281 "read_file" => read_file_params(),
1282 "write_file" => write_file_params(),
1283 "edit_file" => edit_file_params(),
1284 "list_directory" => list_directory_params(),
1285 "search_files" => search_files_params(),
1286 "find_files" => find_files_params(),
1287 "execute_command" => execute_command_params(),
1288 "web_fetch" => web_fetch_params(),
1289 "web_search" => web_search_params(),
1290 "process" => process_params(),
1291 "memory_search" => memory_search_params(),
1292 "memory_get" => memory_get_params(),
1293 "save_memory" => save_memory_params(),
1294 "search_history" => search_history_params(),
1295 "cron" => cron_params(),
1296 "sessions_list" => sessions_list_params(),
1297 "sessions_spawn" => sessions_spawn_params(),
1298 "sessions_send" => sessions_send_params(),
1299 "sessions_history" => sessions_history_params(),
1300 "session_status" => session_status_params(),
1301 "agents_list" => agents_list_params(),
1302 "apply_patch" => apply_patch_params(),
1303 "secrets_list" => secrets_list_params(),
1304 "secrets_get" => secrets_get_params(),
1305 "secrets_store" => secrets_store_params(),
1306 "secrets_set_policy" => secrets_set_policy_params(),
1307 "gateway" => gateway_params(),
1308 "message" => message_params(),
1309 "tts" => tts_params(),
1310 "image" => image_params(),
1311 "nodes" => nodes_params(),
1312 "browser" => browser_params(),
1313 "canvas" => canvas_params(),
1314 "skill_list" => skill_list_params(),
1315 "skill_search" => skill_search_params(),
1316 "skill_install" => skill_install_params(),
1317 "skill_info" => skill_info_params(),
1318 "skill_enable" => skill_enable_params(),
1319 "skill_link_secret" => skill_link_secret_params(),
1320 "skill_create" => skill_create_params(),
1321 "mcp_list" => mcp_tools::mcp_list_params(),
1322 "mcp_connect" => mcp_tools::mcp_connect_params(),
1323 "mcp_disconnect" => mcp_tools::mcp_disconnect_params(),
1324 "task_list" => task_tools::task_list_params(),
1325 "task_status" => task_tools::task_id_param(),
1326 "task_foreground" => task_tools::task_id_param(),
1327 "task_background" => task_tools::task_id_param(),
1328 "task_cancel" => task_tools::task_id_param(),
1329 "task_pause" => task_tools::task_id_param(),
1330 "task_resume" => task_tools::task_id_param(),
1331 "task_input" => task_tools::task_input_params(),
1332 "task_describe" => task_tools::task_describe_params(),
1333 "thread_describe" => thread_describe_params(),
1334 "model_list" => model_tools::model_list_params(),
1335 "model_enable" => model_tools::model_id_param(),
1336 "model_disable" => model_tools::model_id_param(),
1337 "model_set" => model_tools::model_id_param(),
1338 "model_recommend" => model_tools::model_recommend_params(),
1339 "disk_usage" => disk_usage_params(),
1340 "classify_files" => classify_files_params(),
1341 "system_monitor" => system_monitor_params(),
1342 "battery_health" => battery_health_params(),
1343 "app_index" => app_index_params(),
1344 "cloud_browse" => cloud_browse_params(),
1345 "browser_cache" => browser_cache_params(),
1346 "screenshot" => screenshot_params(),
1347 "clipboard" => clipboard_params(),
1348 "audit_sensitive" => audit_sensitive_params(),
1349 "secure_delete" => secure_delete_params(),
1350 "summarize_file" => summarize_file_params(),
1351 "ask_user" => ask_user_params(),
1352 "pkg_manage" => pkg_manage_params(),
1353 "net_info" => net_info_params(),
1354 "net_scan" => net_scan_params(),
1355 "service_manage" => service_manage_params(),
1356 "user_manage" => user_manage_params(),
1357 "firewall" => firewall_params(),
1358 "ollama_manage" => ollama_manage_params(),
1359 "exo_manage" => exo_manage_params(),
1360 "uv_manage" => uv_manage_params(),
1361 "npm_manage" => npm_manage_params(),
1362 "agent_setup" => agent_setup_params(),
1363 "pdf" => pdf_params(),
1364 _ => vec![],
1365 }
1366}
1367
1368pub fn tools_openai() -> Vec<Value> {
1374 all_tools()
1375 .into_iter()
1376 .map(|t| {
1377 let params = resolve_params(t);
1378 let (properties, required) = params_to_json_schema(¶ms);
1379 json!({
1380 "type": "function",
1381 "function": {
1382 "name": t.name,
1383 "description": t.description,
1384 "parameters": {
1385 "type": "object",
1386 "properties": properties,
1387 "required": required,
1388 }
1389 }
1390 })
1391 })
1392 .collect()
1393}
1394
1395pub fn tools_anthropic() -> Vec<Value> {
1401 all_tools()
1402 .into_iter()
1403 .map(|t| {
1404 let params = resolve_params(t);
1405 let (properties, required) = params_to_json_schema(¶ms);
1406 json!({
1407 "name": t.name,
1408 "description": t.description,
1409 "input_schema": {
1410 "type": "object",
1411 "properties": properties,
1412 "required": required,
1413 }
1414 })
1415 })
1416 .collect()
1417}
1418
1419pub fn tools_google() -> Vec<Value> {
1425 all_tools()
1426 .into_iter()
1427 .map(|t| {
1428 let params = resolve_params(t);
1429 let (properties, required) = params_to_json_schema(¶ms);
1430 json!({
1431 "name": t.name,
1432 "description": t.description,
1433 "parameters": {
1434 "type": "object",
1435 "properties": properties,
1436 "required": required,
1437 }
1438 })
1439 })
1440 .collect()
1441}
1442
1443pub fn is_secrets_tool(name: &str) -> bool {
1448 matches!(
1449 name,
1450 "secrets_list" | "secrets_get" | "secrets_store" | "secrets_set_policy"
1451 )
1452}
1453
1454pub fn is_skill_tool(name: &str) -> bool {
1458 matches!(
1459 name,
1460 "skill_list"
1461 | "skill_search"
1462 | "skill_install"
1463 | "skill_info"
1464 | "skill_enable"
1465 | "skill_link_secret"
1466 | "skill_create"
1467 )
1468}
1469
1470pub fn is_user_prompt_tool(name: &str) -> bool {
1473 name == "ask_user"
1474}
1475
1476const ASYNC_NATIVE_TOOLS: &[&str] = &[
1478 "execute_command",
1479 "process",
1480 "web_fetch",
1481 "web_search",
1482 "read_file",
1483 "write_file",
1484 "edit_file",
1485 "list_directory",
1486 "search_files",
1487 "find_files",
1488 "gateway",
1489 "message",
1490 "tts",
1491 "image",
1492 "ollama_manage",
1493 "exo_manage",
1494 "uv_manage",
1495 "npm_manage",
1496 "pkg_manage",
1497 "net_info",
1498 "net_scan",
1499 "service_manage",
1500 "user_manage",
1501 "firewall",
1502 "disk_usage",
1503 "classify_files",
1504 "system_monitor",
1505 "battery_health",
1506 "app_index",
1507 "cloud_browse",
1508 "browser_cache",
1509 "screenshot",
1510 "clipboard",
1511 "audit_sensitive",
1512 "secure_delete",
1513 "summarize_file",
1514 "nodes",
1515 "canvas",
1516];
1517
1518#[instrument(skip(args, workspace_dir), fields(tool = name))]
1523pub async fn execute_tool(
1524 name: &str,
1525 args: &Value,
1526 workspace_dir: &Path,
1527) -> Result<String, String> {
1528 debug!("Executing tool");
1529
1530 if ASYNC_NATIVE_TOOLS.contains(&name) {
1532 let result = match name {
1533 "execute_command" => runtime::exec_execute_command_async(args, workspace_dir).await,
1534 "process" => runtime::exec_process_async(args, workspace_dir).await,
1535 "web_fetch" => web::exec_web_fetch_async(args, workspace_dir).await,
1536 "web_search" => web::exec_web_search_async(args, workspace_dir).await,
1537 "read_file" => file::exec_read_file_async(args, workspace_dir).await,
1538 "write_file" => file::exec_write_file_async(args, workspace_dir).await,
1539 "edit_file" => file::exec_edit_file_async(args, workspace_dir).await,
1540 "list_directory" => file::exec_list_directory_async(args, workspace_dir).await,
1541 "search_files" => file::exec_search_files_async(args, workspace_dir).await,
1542 "find_files" => file::exec_find_files_async(args, workspace_dir).await,
1543 "gateway" => gateway_tools::exec_gateway_async(args, workspace_dir).await,
1544 "message" => gateway_tools::exec_message_async(args, workspace_dir).await,
1545 "tts" => gateway_tools::exec_tts_async(args, workspace_dir).await,
1546 "image" => gateway_tools::exec_image_async(args, workspace_dir).await,
1547 "ollama_manage" => ollama::exec_ollama_manage_async(args, workspace_dir).await,
1548 "exo_manage" => exo_ai::exec_exo_manage_async(args, workspace_dir).await,
1549 "uv_manage" => uv::exec_uv_manage_async(args, workspace_dir).await,
1550 "npm_manage" => npm::exec_npm_manage_async(args, workspace_dir).await,
1551 "pkg_manage" => sysadmin::exec_pkg_manage_async(args, workspace_dir).await,
1552 "net_info" => sysadmin::exec_net_info_async(args, workspace_dir).await,
1553 "net_scan" => sysadmin::exec_net_scan_async(args, workspace_dir).await,
1554 "service_manage" => sysadmin::exec_service_manage_async(args, workspace_dir).await,
1555 "user_manage" => sysadmin::exec_user_manage_async(args, workspace_dir).await,
1556 "firewall" => sysadmin::exec_firewall_async(args, workspace_dir).await,
1557 "disk_usage" => system_tools::exec_disk_usage_async(args, workspace_dir).await,
1558 "classify_files" => system_tools::exec_classify_files_async(args, workspace_dir).await,
1559 "system_monitor" => system_tools::exec_system_monitor_async(args, workspace_dir).await,
1560 "battery_health" => system_tools::exec_battery_health_async(args, workspace_dir).await,
1561 "app_index" => system_tools::exec_app_index_async(args, workspace_dir).await,
1562 "cloud_browse" => system_tools::exec_cloud_browse_async(args, workspace_dir).await,
1563 "browser_cache" => system_tools::exec_browser_cache_async(args, workspace_dir).await,
1564 "screenshot" => system_tools::exec_screenshot_async(args, workspace_dir).await,
1565 "clipboard" => system_tools::exec_clipboard_async(args, workspace_dir).await,
1566 "audit_sensitive" => {
1567 system_tools::exec_audit_sensitive_async(args, workspace_dir).await
1568 }
1569 "secure_delete" => system_tools::exec_secure_delete_async(args, workspace_dir).await,
1570 "summarize_file" => system_tools::exec_summarize_file_async(args, workspace_dir).await,
1571 "nodes" => devices::exec_nodes_async(args, workspace_dir).await,
1572 "canvas" => devices::exec_canvas_async(args, workspace_dir).await,
1573 _ => unreachable!(),
1574 };
1575 if result.is_err() {
1576 warn!(error = ?result.as_ref().err(), "Tool execution failed");
1577 }
1578 return result;
1579 }
1580
1581 let tool = all_tools().into_iter().find(|t| t.name == name);
1583
1584 let Some(tool) = tool else {
1585 warn!(tool = name, "Unknown tool requested");
1586 return Err(format!("Unknown tool: {}", name));
1587 };
1588
1589 let execute_fn = tool.execute;
1591 let args = args.clone();
1592 let workspace_dir = workspace_dir.to_path_buf();
1593
1594 let result = tokio::task::spawn_blocking(move || execute_fn(&args, &workspace_dir))
1596 .await
1597 .map_err(|e| format!("Task join error: {}", e))?;
1598
1599 if result.is_err() {
1600 warn!(error = ?result.as_ref().err(), "Tool execution failed");
1601 }
1602
1603 result
1604}
1605
1606#[derive(Debug, Clone, Serialize, Deserialize)]
1610pub struct ToolCall {
1611 pub id: String,
1612 pub name: String,
1613 pub arguments: Value,
1614}
1615
1616#[derive(Debug, Clone, Serialize, Deserialize)]
1619pub struct ToolResult {
1620 pub id: String,
1621 pub name: String,
1622 pub result: String,
1623 pub is_error: bool,
1624}
1625
1626#[cfg(test)]
1627mod tests {
1628 use super::*;
1629 use std::path::Path;
1630
1631 fn ws() -> &'static Path {
1633 Path::new(env!("CARGO_MANIFEST_DIR"))
1636 .parent()
1637 .unwrap()
1638 .parent()
1639 .unwrap()
1640 }
1641
1642 #[test]
1645 fn test_read_file_this_file() {
1646 let args = json!({ "path": file!(), "start_line": 1, "end_line": 5 });
1647 let result = exec_read_file(&args, ws());
1648 assert!(result.is_ok());
1649 let text = result.unwrap();
1650 assert!(text.contains("Agent tool system"));
1651 }
1652
1653 #[test]
1654 fn test_read_file_missing() {
1655 let args = json!({ "path": "/nonexistent/file.txt" });
1656 let result = exec_read_file(&args, ws());
1657 assert!(result.is_err());
1658 }
1659
1660 #[test]
1661 fn test_read_file_no_path() {
1662 let args = json!({});
1663 let result = exec_read_file(&args, ws());
1664 assert!(result.is_err());
1665 assert!(result.unwrap_err().contains("Missing required parameter"));
1666 }
1667
1668 #[test]
1669 fn test_read_file_relative() {
1670 let args = json!({ "path": "Cargo.toml", "start_line": 1, "end_line": 3 });
1672 let result = exec_read_file(&args, ws());
1673 assert!(result.is_ok());
1674 let text = result.unwrap();
1675 assert!(text.contains("workspace"));
1676 }
1677
1678 #[test]
1681 fn test_write_file_and_read_back() {
1682 let dir = std::env::temp_dir().join("rustyclaw_test_write");
1683 let _ = std::fs::remove_dir_all(&dir);
1684 let args = json!({
1685 "path": "sub/test.txt",
1686 "content": "hello world"
1687 });
1688 let result = exec_write_file(&args, &dir);
1689 assert!(result.is_ok());
1690 assert!(result.unwrap().contains("11 bytes"));
1691
1692 let content = std::fs::read_to_string(dir.join("sub/test.txt")).unwrap();
1693 assert_eq!(content, "hello world");
1694 let _ = std::fs::remove_dir_all(&dir);
1695 }
1696
1697 #[test]
1700 fn test_edit_file_single_match() {
1701 let dir = std::env::temp_dir().join("rustyclaw_test_edit");
1702 let _ = std::fs::remove_dir_all(&dir);
1703 std::fs::create_dir_all(&dir).unwrap();
1704 std::fs::write(dir.join("f.txt"), "aaa\nbbb\nccc\n").unwrap();
1705
1706 let args = json!({ "path": "f.txt", "old_string": "bbb", "new_string": "BBB" });
1707 let result = exec_edit_file(&args, &dir);
1708 assert!(result.is_ok());
1709
1710 let content = std::fs::read_to_string(dir.join("f.txt")).unwrap();
1711 assert_eq!(content, "aaa\nBBB\nccc\n");
1712 let _ = std::fs::remove_dir_all(&dir);
1713 }
1714
1715 #[test]
1716 fn test_edit_file_no_match() {
1717 let dir = std::env::temp_dir().join("rustyclaw_test_edit_no");
1718 let _ = std::fs::remove_dir_all(&dir);
1719 std::fs::create_dir_all(&dir).unwrap();
1720 std::fs::write(dir.join("f.txt"), "aaa\nbbb\n").unwrap();
1721
1722 let args = json!({ "path": "f.txt", "old_string": "zzz", "new_string": "ZZZ" });
1723 let result = exec_edit_file(&args, &dir);
1724 assert!(result.is_err());
1725 assert!(result.unwrap_err().contains("not found"));
1726 let _ = std::fs::remove_dir_all(&dir);
1727 }
1728
1729 #[test]
1730 fn test_edit_file_multiple_matches() {
1731 let dir = std::env::temp_dir().join("rustyclaw_test_edit_multi");
1732 let _ = std::fs::remove_dir_all(&dir);
1733 std::fs::create_dir_all(&dir).unwrap();
1734 std::fs::write(dir.join("f.txt"), "aaa\naaa\n").unwrap();
1735
1736 let args = json!({ "path": "f.txt", "old_string": "aaa", "new_string": "bbb" });
1737 let result = exec_edit_file(&args, &dir);
1738 assert!(result.is_err());
1739 assert!(result.unwrap_err().contains("2 times"));
1740 let _ = std::fs::remove_dir_all(&dir);
1741 }
1742
1743 #[test]
1746 fn test_list_directory() {
1747 let args = json!({ "path": "crates/rustyclaw-core/src" });
1748 let result = exec_list_directory(&args, ws());
1749 assert!(result.is_ok());
1750 let text = result.unwrap();
1751 assert!(text.contains("tools/"));
1753 assert!(text.contains("lib.rs"));
1754 }
1755
1756 #[test]
1759 fn test_search_files_finds_pattern() {
1760 let args = json!({ "pattern": "exec_read_file", "path": "crates/rustyclaw-core/src", "include": "*.rs" });
1761 let result = exec_search_files(&args, ws());
1762 assert!(result.is_ok());
1763 let text = result.unwrap();
1764 assert!(text.contains("tools/file.rs") || text.contains("tools\\file.rs"));
1766 }
1767
1768 #[test]
1769 fn test_search_files_no_match() {
1770 let dir = std::env::temp_dir().join("rustyclaw_test_search_none");
1771 let _ = std::fs::remove_dir_all(&dir);
1772 std::fs::create_dir_all(&dir).unwrap();
1773 std::fs::write(dir.join("a.txt"), "hello world\n").unwrap();
1774
1775 let args = json!({ "pattern": "XYZZY_NEVER_42" });
1776 let result = exec_search_files(&args, &dir);
1777 assert!(result.is_ok());
1778 assert!(result.unwrap().contains("No matches"));
1779 let _ = std::fs::remove_dir_all(&dir);
1780 }
1781
1782 #[test]
1785 fn test_find_files_glob() {
1786 let args = json!({ "pattern": "*.toml" });
1787 let result = exec_find_files(&args, ws());
1788 assert!(result.is_ok());
1789 let text = result.unwrap();
1790 assert!(text.contains("Cargo.toml"));
1791 }
1792
1793 #[test]
1794 fn test_find_files_keyword_case_insensitive() {
1795 let args = json!({ "pattern": "cargo" });
1797 let result = exec_find_files(&args, ws());
1798 assert!(result.is_ok());
1799 let text = result.unwrap();
1800 assert!(text.contains("Cargo.toml"));
1801 }
1802
1803 #[test]
1804 fn test_find_files_multiple_keywords() {
1805 let args = json!({ "pattern": "cargo license" });
1807 let result = exec_find_files(&args, ws());
1808 assert!(result.is_ok());
1809 let text = result.unwrap();
1810 assert!(text.contains("Cargo.toml"));
1811 assert!(text.contains("LICENSE"));
1812 }
1813
1814 #[test]
1815 fn test_find_files_keyword_no_match() {
1816 let dir = std::env::temp_dir().join("rustyclaw_test_find_kw");
1817 let _ = std::fs::remove_dir_all(&dir);
1818 std::fs::create_dir_all(&dir).unwrap();
1819 std::fs::write(dir.join("hello.txt"), "content").unwrap();
1820
1821 let args = json!({ "pattern": "resume" });
1822 let result = exec_find_files(&args, &dir);
1823 assert!(result.is_ok());
1824 assert!(result.unwrap().contains("No files found"));
1825 let _ = std::fs::remove_dir_all(&dir);
1826 }
1827
1828 #[test]
1831 fn test_execute_command_echo() {
1832 let args = json!({ "command": "echo hello" });
1833 let result = exec_execute_command(&args, ws());
1834 assert!(result.is_ok());
1835 assert!(result.unwrap().contains("hello"));
1836 }
1837
1838 #[test]
1839 fn test_execute_command_failure() {
1840 let args = json!({ "command": "false" });
1841 let result = exec_execute_command(&args, ws());
1842 assert!(result.is_ok()); assert!(result.unwrap().contains("exit code"));
1844 }
1845
1846 #[tokio::test]
1849 async fn test_execute_tool_dispatch() {
1850 let args = json!({ "path": file!() });
1851 let result = execute_tool("read_file", &args, ws()).await;
1852 assert!(result.is_ok());
1853 }
1854
1855 #[tokio::test]
1856 async fn test_execute_tool_unknown() {
1857 let result = execute_tool("no_such_tool", &json!({}), ws()).await;
1858 assert!(result.is_err());
1859 }
1860
1861 #[test]
1864 fn test_openai_format() {
1865 let tools = tools_openai();
1866 assert!(
1867 tools.len() >= 60,
1868 "Expected at least 60 tools, got {}",
1869 tools.len()
1870 );
1871 assert_eq!(tools[0]["type"], "function");
1872 assert_eq!(tools[0]["function"]["name"], "read_file");
1873 assert!(tools[0]["function"]["parameters"]["properties"]["path"].is_object());
1874 }
1875
1876 #[test]
1877 fn test_anthropic_format() {
1878 let tools = tools_anthropic();
1879 assert!(
1880 tools.len() >= 60,
1881 "Expected at least 60 tools, got {}",
1882 tools.len()
1883 );
1884 assert_eq!(tools[0]["name"], "read_file");
1885 assert!(tools[0]["input_schema"]["properties"]["path"].is_object());
1886 }
1887
1888 #[test]
1889 fn test_google_format() {
1890 let tools = tools_google();
1891 assert!(
1892 tools.len() >= 60,
1893 "Expected at least 60 tools, got {}",
1894 tools.len()
1895 );
1896 assert_eq!(tools[0]["name"], "read_file");
1897 }
1898
1899 #[test]
1902 fn test_resolve_path_absolute() {
1903 let result = helpers::resolve_path(Path::new("/workspace"), "/absolute/path.txt");
1904 assert_eq!(result, std::path::PathBuf::from("/absolute/path.txt"));
1905 }
1906
1907 #[test]
1908 fn test_resolve_path_relative() {
1909 let result = helpers::resolve_path(Path::new("/workspace"), "relative/path.txt");
1910 assert_eq!(
1911 result,
1912 std::path::PathBuf::from("/workspace/relative/path.txt")
1913 );
1914 }
1915
1916 #[test]
1919 fn test_web_fetch_missing_url() {
1920 let args = json!({});
1921 let result = exec_web_fetch(&args, ws());
1922 assert!(result.is_err());
1923 assert!(result.unwrap_err().contains("Missing required parameter"));
1924 }
1925
1926 #[test]
1927 fn test_web_fetch_invalid_url() {
1928 let args = json!({ "url": "not-a-url" });
1929 let result = exec_web_fetch(&args, ws());
1930 assert!(result.is_err());
1931 assert!(result.unwrap_err().contains("http"));
1932 }
1933
1934 #[test]
1935 fn test_web_fetch_params_defined() {
1936 let params = web_fetch_params();
1937 assert_eq!(params.len(), 6);
1938 assert!(params.iter().any(|p| p.name == "url" && p.required));
1939 assert!(
1940 params
1941 .iter()
1942 .any(|p| p.name == "extract_mode" && !p.required)
1943 );
1944 assert!(params.iter().any(|p| p.name == "max_chars" && !p.required));
1945 assert!(
1946 params
1947 .iter()
1948 .any(|p| p.name == "use_cookies" && !p.required)
1949 );
1950 assert!(
1951 params
1952 .iter()
1953 .any(|p| p.name == "authorization" && !p.required)
1954 );
1955 assert!(params.iter().any(|p| p.name == "headers" && !p.required));
1956 }
1957
1958 #[test]
1961 fn test_web_search_missing_query() {
1962 let args = json!({});
1963 let result = exec_web_search(&args, ws());
1964 assert!(result.is_err());
1965 assert!(result.unwrap_err().contains("Missing required parameter"));
1966 }
1967
1968 #[test]
1969 fn test_web_search_no_api_key() {
1970 unsafe { std::env::remove_var("BRAVE_API_KEY") };
1973 let args = json!({ "query": "test" });
1974 let result = exec_web_search(&args, ws());
1975 assert!(result.is_err());
1976 assert!(result.unwrap_err().contains("BRAVE_API_KEY"));
1977 }
1978
1979 #[test]
1980 fn test_web_search_params_defined() {
1981 let params = web_search_params();
1982 assert_eq!(params.len(), 5);
1983 assert!(params.iter().any(|p| p.name == "query" && p.required));
1984 assert!(params.iter().any(|p| p.name == "count" && !p.required));
1985 assert!(params.iter().any(|p| p.name == "country" && !p.required));
1986 assert!(
1987 params
1988 .iter()
1989 .any(|p| p.name == "search_lang" && !p.required)
1990 );
1991 assert!(params.iter().any(|p| p.name == "freshness" && !p.required));
1992 }
1993
1994 #[test]
1997 fn test_process_missing_action() {
1998 let args = json!({});
1999 let result = exec_process(&args, ws());
2000 assert!(result.is_err());
2001 assert!(result.unwrap_err().contains("Missing required parameter"));
2002 }
2003
2004 #[test]
2005 fn test_process_invalid_action() {
2006 let args = json!({ "action": "invalid" });
2007 let result = exec_process(&args, ws());
2008 assert!(result.is_err());
2009 assert!(result.unwrap_err().contains("Unknown action"));
2010 }
2011
2012 #[test]
2013 fn test_process_list_empty() {
2014 let args = json!({ "action": "list" });
2015 let result = exec_process(&args, ws());
2016 assert!(result.is_ok());
2017 }
2019
2020 #[test]
2021 fn test_process_params_defined() {
2022 let params = process_params();
2023 assert_eq!(params.len(), 6);
2024 assert!(params.iter().any(|p| p.name == "action" && p.required));
2025 assert!(params.iter().any(|p| p.name == "sessionId" && !p.required));
2026 assert!(params.iter().any(|p| p.name == "data" && !p.required));
2027 assert!(params.iter().any(|p| p.name == "keys" && !p.required));
2028 assert!(params.iter().any(|p| p.name == "offset" && !p.required));
2029 assert!(params.iter().any(|p| p.name == "limit" && !p.required));
2030 }
2031
2032 #[test]
2033 fn test_execute_command_params_with_background() {
2034 let params = execute_command_params();
2035 assert_eq!(params.len(), 5);
2036 assert!(params.iter().any(|p| p.name == "command" && p.required));
2037 assert!(params.iter().any(|p| p.name == "background" && !p.required));
2038 assert!(params.iter().any(|p| p.name == "yieldMs" && !p.required));
2039 }
2040
2041 #[test]
2044 fn test_memory_search_params_defined() {
2045 let params = memory_search_params();
2046 assert_eq!(params.len(), 5);
2047 assert!(params.iter().any(|p| p.name == "query" && p.required));
2048 assert!(params.iter().any(|p| p.name == "maxResults" && !p.required));
2049 assert!(params.iter().any(|p| p.name == "minScore" && !p.required));
2050 assert!(
2051 params
2052 .iter()
2053 .any(|p| p.name == "recencyBoost" && !p.required)
2054 );
2055 assert!(
2056 params
2057 .iter()
2058 .any(|p| p.name == "halfLifeDays" && !p.required)
2059 );
2060 }
2061
2062 #[test]
2063 fn test_memory_search_missing_query() {
2064 let args = json!({});
2065 let result = exec_memory_search(&args, ws());
2066 assert!(result.is_err());
2067 assert!(result.unwrap_err().contains("Missing required parameter"));
2068 }
2069
2070 #[test]
2073 fn test_memory_get_params_defined() {
2074 let params = memory_get_params();
2075 assert_eq!(params.len(), 3);
2076 assert!(params.iter().any(|p| p.name == "path" && p.required));
2077 assert!(params.iter().any(|p| p.name == "from" && !p.required));
2078 assert!(params.iter().any(|p| p.name == "lines" && !p.required));
2079 }
2080
2081 #[test]
2082 fn test_memory_get_missing_path() {
2083 let args = json!({});
2084 let result = exec_memory_get(&args, ws());
2085 assert!(result.is_err());
2086 assert!(result.unwrap_err().contains("Missing required parameter"));
2087 }
2088
2089 #[test]
2090 fn test_memory_get_invalid_path() {
2091 let args = json!({ "path": "../etc/passwd" });
2092 let result = exec_memory_get(&args, ws());
2093 assert!(result.is_err());
2094 assert!(result.unwrap_err().contains("not a valid memory file"));
2095 }
2096
2097 #[test]
2100 fn test_cron_params_defined() {
2101 let params = cron_params();
2102 assert_eq!(params.len(), 5);
2103 assert!(params.iter().any(|p| p.name == "action" && p.required));
2104 assert!(params.iter().any(|p| p.name == "jobId" && !p.required));
2105 }
2106
2107 #[test]
2108 fn test_cron_missing_action() {
2109 let args = json!({});
2110 let result = exec_cron(&args, ws());
2111 assert!(result.is_err());
2112 assert!(result.unwrap_err().contains("Missing required parameter"));
2113 }
2114
2115 #[test]
2116 fn test_cron_invalid_action() {
2117 let args = json!({ "action": "invalid" });
2118 let result = exec_cron(&args, ws());
2119 assert!(result.is_err());
2120 assert!(result.unwrap_err().contains("Unknown action"));
2121 }
2122
2123 #[test]
2126 fn test_sessions_list_params_defined() {
2127 let params = sessions_list_params();
2128 assert_eq!(params.len(), 4);
2129 assert!(params.iter().all(|p| !p.required));
2130 }
2131
2132 #[test]
2135 fn test_sessions_spawn_params_defined() {
2136 let params = sessions_spawn_params();
2137 assert_eq!(params.len(), 7);
2138 assert!(params.iter().any(|p| p.name == "task" && p.required));
2139 }
2140
2141 #[test]
2142 fn test_sessions_spawn_missing_task() {
2143 let args = json!({});
2144 let result = exec_sessions_spawn(&args, ws());
2145 assert!(result.is_err());
2146 assert!(result.unwrap_err().contains("Missing required parameter"));
2147 }
2148
2149 #[test]
2152 fn test_sessions_send_params_defined() {
2153 let params = sessions_send_params();
2154 assert_eq!(params.len(), 4);
2155 assert!(params.iter().any(|p| p.name == "message" && p.required));
2156 }
2157
2158 #[test]
2159 fn test_sessions_send_missing_message() {
2160 let args = json!({});
2161 let result = exec_sessions_send(&args, ws());
2162 assert!(result.is_err());
2163 assert!(result.unwrap_err().contains("Missing required parameter"));
2164 }
2165
2166 #[test]
2169 fn test_sessions_history_params_defined() {
2170 let params = sessions_history_params();
2171 assert_eq!(params.len(), 3);
2172 assert!(params.iter().any(|p| p.name == "sessionKey" && p.required));
2173 }
2174
2175 #[test]
2178 fn test_session_status_params_defined() {
2179 let params = session_status_params();
2180 assert_eq!(params.len(), 2);
2181 assert!(params.iter().all(|p| !p.required));
2182 }
2183
2184 #[test]
2185 fn test_session_status_general() {
2186 let args = json!({});
2187 let result = exec_session_status(&args, ws());
2188 assert!(result.is_ok());
2189 assert!(result.unwrap().contains("Session Status"));
2190 }
2191
2192 #[test]
2195 fn test_agents_list_params_defined() {
2196 let params = agents_list_params();
2197 assert_eq!(params.len(), 0);
2198 }
2199
2200 #[test]
2201 fn test_agents_list_returns_main() {
2202 let args = json!({});
2203 let result = exec_agents_list(&args, ws());
2204 assert!(result.is_ok());
2205 assert!(result.unwrap().contains("main"));
2206 }
2207
2208 #[test]
2211 fn test_apply_patch_params_defined() {
2212 let params = apply_patch_params();
2213 assert_eq!(params.len(), 3);
2214 assert!(params.iter().any(|p| p.name == "patch" && p.required));
2215 assert!(params.iter().any(|p| p.name == "dry_run" && !p.required));
2216 }
2217
2218 #[test]
2219 fn test_apply_patch_missing_patch() {
2220 let args = json!({});
2221 let result = exec_apply_patch(&args, ws());
2222 assert!(result.is_err());
2223 assert!(result.unwrap_err().contains("Missing required parameter"));
2224 }
2225
2226 #[test]
2227 fn test_parse_unified_diff() {
2228 let patch_str = r#"--- a/test.txt
2229+++ b/test.txt
2230@@ -1,3 +1,4 @@
2231 line1
2232+new line
2233 line2
2234 line3
2235"#;
2236 let hunks = patch::parse_unified_diff(patch_str).unwrap();
2237 assert_eq!(hunks.len(), 1);
2238 assert_eq!(hunks[0].file_path, "test.txt");
2239 assert_eq!(hunks[0].old_start, 1);
2240 assert_eq!(hunks[0].old_count, 3);
2241 }
2242
2243 #[test]
2246 fn test_secrets_stub_rejects() {
2247 let args = json!({});
2248 let result = exec_secrets_stub(&args, ws());
2249 assert!(result.is_err());
2250 assert!(result.unwrap_err().contains("gateway"));
2251 }
2252
2253 #[test]
2254 fn test_is_secrets_tool() {
2255 assert!(is_secrets_tool("secrets_list"));
2256 assert!(is_secrets_tool("secrets_get"));
2257 assert!(is_secrets_tool("secrets_store"));
2258 assert!(!is_secrets_tool("read_file"));
2259 assert!(!is_secrets_tool("memory_get"));
2260 }
2261
2262 #[test]
2263 fn test_secrets_list_params_defined() {
2264 let params = secrets_list_params();
2265 assert_eq!(params.len(), 1);
2266 assert!(params.iter().any(|p| p.name == "prefix" && !p.required));
2267 }
2268
2269 #[test]
2270 fn test_secrets_get_params_defined() {
2271 let params = secrets_get_params();
2272 assert_eq!(params.len(), 1);
2273 assert!(params.iter().any(|p| p.name == "name" && p.required));
2274 }
2275
2276 #[test]
2277 fn test_secrets_store_params_defined() {
2278 let params = secrets_store_params();
2279 assert_eq!(params.len(), 6);
2280 assert!(params.iter().any(|p| p.name == "name" && p.required));
2281 assert!(params.iter().any(|p| p.name == "kind" && p.required));
2282 assert!(params.iter().any(|p| p.name == "value" && p.required));
2283 assert!(params.iter().any(|p| p.name == "policy" && !p.required));
2284 assert!(
2285 params
2286 .iter()
2287 .any(|p| p.name == "description" && !p.required)
2288 );
2289 assert!(params.iter().any(|p| p.name == "username" && !p.required));
2290 }
2291
2292 #[test]
2293 fn test_protected_path_without_init() {
2294 assert!(!is_protected_path(Path::new("/some/random/path")));
2296 }
2297
2298 #[test]
2301 fn test_gateway_params_defined() {
2302 let params = gateway_params();
2303 assert_eq!(params.len(), 5);
2304 assert!(params.iter().any(|p| p.name == "action" && p.required));
2305 }
2306
2307 #[test]
2308 fn test_gateway_missing_action() {
2309 let args = json!({});
2310 let result = exec_gateway(&args, ws());
2311 assert!(result.is_err());
2312 assert!(result.unwrap_err().contains("Missing required parameter"));
2313 }
2314
2315 #[test]
2316 fn test_gateway_config_schema() {
2317 let args = json!({ "action": "config.schema" });
2318 let result = exec_gateway(&args, ws());
2319 assert!(result.is_ok());
2320 assert!(result.unwrap().contains("properties"));
2321 }
2322
2323 #[test]
2326 fn test_message_params_defined() {
2327 let params = message_params();
2328 assert_eq!(params.len(), 7);
2329 assert!(params.iter().any(|p| p.name == "action" && p.required));
2330 }
2331
2332 #[test]
2333 fn test_message_missing_action() {
2334 let args = json!({});
2335 let result = exec_message(&args, ws());
2336 assert!(result.is_err());
2337 assert!(result.unwrap_err().contains("Missing required parameter"));
2338 }
2339
2340 #[test]
2343 fn test_tts_params_defined() {
2344 let params = tts_params();
2345 assert_eq!(params.len(), 2);
2346 assert!(params.iter().any(|p| p.name == "text" && p.required));
2347 }
2348
2349 #[test]
2350 fn test_tts_missing_text() {
2351 let args = json!({});
2352 let result = exec_tts(&args, ws());
2353 assert!(result.is_err());
2354 assert!(result.unwrap_err().contains("Missing required parameter"));
2355 }
2356
2357 #[test]
2358 fn test_tts_returns_media_path() {
2359 let args = json!({ "text": "Hello world" });
2360 let result = exec_tts(&args, ws());
2361 assert!(result.is_ok());
2362 assert!(result.unwrap().contains("MEDIA:"));
2363 }
2364
2365 #[test]
2368 fn test_image_params_defined() {
2369 let params = image_params();
2370 assert_eq!(params.len(), 2);
2371 assert!(params.iter().any(|p| p.name == "image" && p.required));
2372 assert!(params.iter().any(|p| p.name == "prompt" && !p.required));
2373 }
2374
2375 #[test]
2376 fn test_image_missing_image() {
2377 let args = json!({});
2378 let result = exec_image(&args, ws());
2379 assert!(result.is_err());
2380 assert!(result.unwrap_err().contains("Missing required parameter"));
2381 }
2382
2383 #[test]
2384 fn test_image_url_detection() {
2385 let args = json!({ "image": "https://example.com/photo.jpg" });
2386 let result = exec_image(&args, ws());
2387 assert!(result.is_ok());
2388 assert!(result.unwrap().contains("Is URL: true"));
2389 }
2390
2391 #[test]
2394 fn test_nodes_params_defined() {
2395 let params = nodes_params();
2396 assert_eq!(params.len(), 8);
2397 assert!(params.iter().any(|p| p.name == "action" && p.required));
2398 assert!(params.iter().any(|p| p.name == "node" && !p.required));
2399 }
2400
2401 #[test]
2402 fn test_nodes_missing_action() {
2403 let args = json!({});
2404 let result = exec_nodes(&args, ws());
2405 assert!(result.is_err());
2406 assert!(result.unwrap_err().contains("Missing required parameter"));
2407 }
2408
2409 #[test]
2410 fn test_nodes_status() {
2411 let args = json!({ "action": "status" });
2412 let result = exec_nodes(&args, ws());
2413 assert!(result.is_ok());
2414 let output = result.unwrap();
2415 assert!(output.contains("nodes"));
2416 assert!(output.contains("tools"));
2417 }
2418
2419 #[test]
2422 fn test_browser_params_defined() {
2423 let params = browser_params();
2424 assert_eq!(params.len(), 7);
2425 assert!(params.iter().any(|p| p.name == "action" && p.required));
2426 }
2427
2428 #[test]
2429 fn test_browser_missing_action() {
2430 let args = json!({});
2431 let result = exec_browser(&args, ws());
2432 assert!(result.is_err());
2433 assert!(result.unwrap_err().contains("Missing required parameter"));
2434 }
2435
2436 #[test]
2437 fn test_browser_status() {
2438 let args = json!({ "action": "status" });
2439 let result = exec_browser(&args, ws());
2440 assert!(result.is_ok());
2441 let output = result.unwrap();
2442 assert!(output.contains("running"));
2443 }
2444
2445 #[test]
2448 fn test_canvas_params_defined() {
2449 let params = canvas_params();
2450 assert_eq!(params.len(), 6);
2451 assert!(params.iter().any(|p| p.name == "action" && p.required));
2452 }
2453
2454 #[test]
2455 fn test_canvas_missing_action() {
2456 let args = json!({});
2457 let result = exec_canvas(&args, ws());
2458 assert!(result.is_err());
2459 assert!(result.unwrap_err().contains("Missing required parameter"));
2460 }
2461
2462 #[test]
2463 fn test_canvas_snapshot() {
2464 let args = json!({ "action": "snapshot" });
2465 let result = exec_canvas(&args, ws());
2466 assert!(result.is_ok());
2467 let output = result.unwrap();
2468 assert!(output.contains("no_canvas") || output.contains("snapshot_captured"));
2470 }
2471
2472 #[test]
2475 fn test_skill_list_params_defined() {
2476 let params = skill_list_params();
2477 assert_eq!(params.len(), 1);
2478 assert!(params.iter().any(|p| p.name == "filter" && !p.required));
2479 }
2480
2481 #[test]
2482 fn test_skill_search_params_defined() {
2483 let params = skill_search_params();
2484 assert_eq!(params.len(), 1);
2485 assert!(params.iter().any(|p| p.name == "query" && p.required));
2486 }
2487
2488 #[test]
2489 fn test_skill_install_params_defined() {
2490 let params = skill_install_params();
2491 assert_eq!(params.len(), 2);
2492 assert!(params.iter().any(|p| p.name == "name" && p.required));
2493 assert!(params.iter().any(|p| p.name == "version" && !p.required));
2494 }
2495
2496 #[test]
2497 fn test_skill_info_params_defined() {
2498 let params = skill_info_params();
2499 assert_eq!(params.len(), 1);
2500 assert!(params.iter().any(|p| p.name == "name" && p.required));
2501 }
2502
2503 #[test]
2504 fn test_skill_enable_params_defined() {
2505 let params = skill_enable_params();
2506 assert_eq!(params.len(), 2);
2507 assert!(params.iter().any(|p| p.name == "name" && p.required));
2508 assert!(params.iter().any(|p| p.name == "enabled" && p.required));
2509 }
2510
2511 #[test]
2512 fn test_skill_link_secret_params_defined() {
2513 let params = skill_link_secret_params();
2514 assert_eq!(params.len(), 3);
2515 assert!(params.iter().any(|p| p.name == "action" && p.required));
2516 assert!(params.iter().any(|p| p.name == "skill" && p.required));
2517 assert!(params.iter().any(|p| p.name == "secret" && p.required));
2518 }
2519
2520 #[test]
2521 fn test_skill_list_standalone_stub() {
2522 let result = exec_skill_list(&json!({}), ws());
2523 assert!(result.is_ok());
2524 assert!(result.unwrap().contains("standalone mode"));
2525 }
2526
2527 #[test]
2528 fn test_skill_search_missing_query() {
2529 let result = exec_skill_search(&json!({}), ws());
2530 assert!(result.is_err());
2531 assert!(result.unwrap_err().contains("Missing required parameter"));
2532 }
2533
2534 #[test]
2535 fn test_skill_install_missing_name() {
2536 let result = exec_skill_install(&json!({}), ws());
2537 assert!(result.is_err());
2538 assert!(result.unwrap_err().contains("Missing required parameter"));
2539 }
2540
2541 #[test]
2542 fn test_skill_info_missing_name() {
2543 let result = exec_skill_info(&json!({}), ws());
2544 assert!(result.is_err());
2545 assert!(result.unwrap_err().contains("Missing required parameter"));
2546 }
2547
2548 #[test]
2549 fn test_skill_enable_missing_params() {
2550 let result = exec_skill_enable(&json!({}), ws());
2551 assert!(result.is_err());
2552 }
2553
2554 #[test]
2555 fn test_skill_link_secret_bad_action() {
2556 let args = json!({ "action": "nope", "skill": "x", "secret": "y" });
2557 let result = exec_skill_link_secret(&args, ws());
2558 assert!(result.is_err());
2559 assert!(result.unwrap_err().contains("Unknown action"));
2560 }
2561
2562 #[test]
2563 fn test_is_skill_tool() {
2564 assert!(is_skill_tool("skill_list"));
2565 assert!(is_skill_tool("skill_search"));
2566 assert!(is_skill_tool("skill_install"));
2567 assert!(is_skill_tool("skill_info"));
2568 assert!(is_skill_tool("skill_enable"));
2569 assert!(is_skill_tool("skill_link_secret"));
2570 assert!(!is_skill_tool("read_file"));
2571 assert!(!is_skill_tool("secrets_list"));
2572 }
2573
2574 #[test]
2577 fn test_disk_usage_params_defined() {
2578 let params = disk_usage_params();
2579 assert_eq!(params.len(), 3);
2580 assert!(params.iter().all(|p| !p.required));
2581 }
2582
2583 #[test]
2584 fn test_disk_usage_workspace() {
2585 let args = json!({ "path": ".", "depth": 1, "top": 5 });
2586 let result = exec_disk_usage(&args, ws());
2587 assert!(result.is_ok());
2588 assert!(result.unwrap().contains("entries"));
2589 }
2590
2591 #[test]
2592 fn test_disk_usage_nonexistent() {
2593 let args = json!({ "path": "/nonexistent_path_xyz" });
2594 let result = exec_disk_usage(&args, ws());
2595 assert!(result.is_err());
2596 }
2597
2598 #[test]
2601 fn test_classify_files_params_defined() {
2602 let params = classify_files_params();
2603 assert_eq!(params.len(), 1);
2604 assert!(params[0].required);
2605 }
2606
2607 #[test]
2608 fn test_classify_files_workspace() {
2609 let args = json!({ "path": "." });
2610 let result = exec_classify_files(&args, ws());
2611 assert!(result.is_ok());
2612 let text = result.unwrap();
2613 assert!(text.contains("path"));
2614 }
2615
2616 #[test]
2617 fn test_classify_files_missing_path() {
2618 let args = json!({});
2619 let result = exec_classify_files(&args, ws());
2620 assert!(result.is_err());
2621 assert!(result.unwrap_err().contains("Missing required parameter"));
2622 }
2623
2624 #[test]
2627 fn test_system_monitor_params_defined() {
2628 let params = system_monitor_params();
2629 assert_eq!(params.len(), 1);
2630 assert!(!params[0].required);
2631 }
2632
2633 #[test]
2634 fn test_system_monitor_all() {
2635 let args = json!({});
2636 let result = exec_system_monitor(&args, ws());
2637 assert!(result.is_ok());
2638 }
2639
2640 #[test]
2641 fn test_system_monitor_cpu() {
2642 let args = json!({ "metric": "cpu" });
2643 let result = exec_system_monitor(&args, ws());
2644 assert!(result.is_ok());
2645 }
2646
2647 #[test]
2650 fn test_battery_health_params_defined() {
2651 let params = battery_health_params();
2652 assert_eq!(params.len(), 0);
2653 }
2654
2655 #[test]
2656 fn test_battery_health_runs() {
2657 let args = json!({});
2658 let result = exec_battery_health(&args, ws());
2659 assert!(result.is_ok());
2660 }
2661
2662 #[test]
2665 fn test_app_index_params_defined() {
2666 let params = app_index_params();
2667 assert_eq!(params.len(), 2);
2668 assert!(params.iter().all(|p| !p.required));
2669 }
2670
2671 #[test]
2672 fn test_app_index_runs() {
2673 let args = json!({ "filter": "nonexistent_app_xyz" });
2674 let result = exec_app_index(&args, ws());
2675 assert!(result.is_ok());
2676 assert!(result.unwrap().contains("apps"));
2677 }
2678
2679 #[test]
2682 fn test_cloud_browse_params_defined() {
2683 let params = cloud_browse_params();
2684 assert_eq!(params.len(), 2);
2685 assert!(params.iter().all(|p| !p.required));
2686 }
2687
2688 #[test]
2689 fn test_cloud_browse_detect() {
2690 let args = json!({ "action": "detect" });
2691 let result = exec_cloud_browse(&args, ws());
2692 assert!(result.is_ok());
2693 assert!(result.unwrap().contains("cloud_folders"));
2694 }
2695
2696 #[test]
2697 fn test_cloud_browse_invalid_action() {
2698 let args = json!({ "action": "invalid" });
2699 let result = exec_cloud_browse(&args, ws());
2700 assert!(result.is_err());
2701 }
2702
2703 #[test]
2706 fn test_browser_cache_params_defined() {
2707 let params = browser_cache_params();
2708 assert_eq!(params.len(), 2);
2709 assert!(params.iter().all(|p| !p.required));
2710 }
2711
2712 #[test]
2713 fn test_browser_cache_scan() {
2714 let args = json!({ "action": "scan" });
2715 let result = exec_browser_cache(&args, ws());
2716 assert!(result.is_ok());
2717 assert!(result.unwrap().contains("caches"));
2718 }
2719
2720 #[test]
2723 fn test_screenshot_params_defined() {
2724 let params = screenshot_params();
2725 assert_eq!(params.len(), 3);
2726 assert!(params.iter().all(|p| !p.required));
2727 }
2728
2729 #[test]
2732 fn test_clipboard_params_defined() {
2733 let params = clipboard_params();
2734 assert_eq!(params.len(), 2);
2735 assert!(params.iter().any(|p| p.name == "action" && p.required));
2736 }
2737
2738 #[test]
2739 fn test_clipboard_missing_action() {
2740 let args = json!({});
2741 let result = exec_clipboard(&args, ws());
2742 assert!(result.is_err());
2743 assert!(result.unwrap_err().contains("Missing required parameter"));
2744 }
2745
2746 #[test]
2747 fn test_clipboard_invalid_action() {
2748 let args = json!({ "action": "invalid" });
2749 let result = exec_clipboard(&args, ws());
2750 assert!(result.is_err());
2751 }
2752
2753 #[test]
2756 fn test_audit_sensitive_params_defined() {
2757 let params = audit_sensitive_params();
2758 assert_eq!(params.len(), 2);
2759 assert!(params.iter().all(|p| !p.required));
2760 }
2761
2762 #[test]
2763 fn test_audit_sensitive_runs() {
2764 let dir = std::env::temp_dir().join("rustyclaw_test_audit");
2765 let _ = std::fs::remove_dir_all(&dir);
2766 std::fs::create_dir_all(&dir).unwrap();
2767 std::fs::write(dir.join("safe.txt"), "nothing sensitive here").unwrap();
2768 let args = json!({ "path": ".", "max_files": 10 });
2769 let result = exec_audit_sensitive(&args, &dir);
2770 assert!(result.is_ok());
2771 assert!(result.unwrap().contains("scanned_files"));
2772 let _ = std::fs::remove_dir_all(&dir);
2773 }
2774
2775 #[test]
2778 fn test_secure_delete_params_defined() {
2779 let params = secure_delete_params();
2780 assert_eq!(params.len(), 3);
2781 assert!(params.iter().any(|p| p.name == "path" && p.required));
2782 }
2783
2784 #[test]
2785 fn test_secure_delete_missing_path() {
2786 let args = json!({});
2787 let result = exec_secure_delete(&args, ws());
2788 assert!(result.is_err());
2789 assert!(result.unwrap_err().contains("Missing required parameter"));
2790 }
2791
2792 #[test]
2793 fn test_secure_delete_nonexistent() {
2794 let args = json!({ "path": "/tmp/nonexistent_rustyclaw_xyz" });
2795 let result = exec_secure_delete(&args, ws());
2796 assert!(result.is_err());
2797 }
2798
2799 #[test]
2800 fn test_secure_delete_requires_confirm() {
2801 let dir = std::env::temp_dir().join("rustyclaw_test_secdelete");
2802 let _ = std::fs::remove_dir_all(&dir);
2803 std::fs::create_dir_all(&dir).unwrap();
2804 std::fs::write(dir.join("victim.txt"), "data").unwrap();
2805 let args = json!({ "path": dir.join("victim.txt").display().to_string() });
2806 let result = exec_secure_delete(&args, ws());
2807 assert!(result.is_ok());
2808 assert!(result.unwrap().contains("confirm_required"));
2809 let _ = std::fs::remove_dir_all(&dir);
2810 }
2811
2812 #[test]
2813 fn test_secure_delete_with_confirm() {
2814 let dir = std::env::temp_dir().join("rustyclaw_test_secdelete2");
2815 let _ = std::fs::remove_dir_all(&dir);
2816 std::fs::create_dir_all(&dir).unwrap();
2817 let victim = dir.join("victim.txt");
2818 std::fs::write(&victim, "secret data").unwrap();
2819 let args = json!({
2820 "path": victim.display().to_string(),
2821 "confirm": true,
2822 });
2823 let result = exec_secure_delete(&args, ws());
2824 assert!(result.is_ok());
2825 assert!(result.unwrap().contains("deleted"));
2826 assert!(!victim.exists());
2827 let _ = std::fs::remove_dir_all(&dir);
2828 }
2829
2830 #[test]
2833 fn test_summarize_file_params_defined() {
2834 let params = summarize_file_params();
2835 assert_eq!(params.len(), 2);
2836 assert!(params.iter().any(|p| p.name == "path" && p.required));
2837 }
2838
2839 #[test]
2840 fn test_summarize_file_missing_path() {
2841 let args = json!({});
2842 let result = exec_summarize_file(&args, ws());
2843 assert!(result.is_err());
2844 assert!(result.unwrap_err().contains("Missing required parameter"));
2845 }
2846
2847 #[test]
2848 fn test_summarize_file_this_file() {
2849 let args = json!({ "path": file!(), "max_lines": 10 });
2850 let result = exec_summarize_file(&args, ws());
2851 assert!(result.is_ok());
2852 let text = result.unwrap();
2853 assert!(text.contains("text"));
2854 assert!(text.contains("total_lines"));
2855 }
2856
2857 #[test]
2858 fn test_summarize_file_nonexistent() {
2859 let args = json!({ "path": "/nonexistent/file.txt" });
2860 let result = exec_summarize_file(&args, ws());
2861 assert!(result.is_err());
2862 }
2863}