git_iris/agents/tools/
docs.rs

1//! Project documentation tool for Rig-based agents
2//!
3//! This tool fetches documentation files like README.md, CONTRIBUTING.md,
4//! CHANGELOG.md, etc. from the project root.
5
6use anyhow::Result;
7use rig::completion::ToolDefinition;
8use rig::tool::Tool;
9use serde::{Deserialize, Serialize};
10use std::path::PathBuf;
11
12use super::common::parameters_schema;
13
14// Use standard tool error macro for consistency
15crate::define_tool_error!(DocsError);
16
17/// Tool for fetching project documentation files
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ProjectDocs;
20
21/// Type of documentation to fetch
22#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema, Default)]
23#[serde(rename_all = "lowercase")]
24pub enum DocType {
25    /// README file (README.md, README.rst, README.txt)
26    #[default]
27    Readme,
28    /// Contributing guidelines (CONTRIBUTING.md)
29    Contributing,
30    /// Changelog (CHANGELOG.md, HISTORY.md)
31    Changelog,
32    /// License file (LICENSE, LICENSE.md)
33    License,
34    /// Code of conduct (`CODE_OF_CONDUCT.md`)
35    CodeOfConduct,
36    /// Agent/AI instructions (AGENTS.md, CLAUDE.md, .github/copilot-instructions.md)
37    Agents,
38    /// Project context: README + agent instructions (recommended for all operations)
39    Context,
40    /// All documentation files
41    All,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
45pub struct ProjectDocsArgs {
46    /// Type of documentation to fetch
47    #[serde(default)]
48    pub doc_type: DocType,
49    /// Maximum characters to return (default: 5000, max: 20000)
50    #[serde(default = "default_max_chars")]
51    pub max_chars: usize,
52}
53
54fn default_max_chars() -> usize {
55    5000
56}
57
58impl Tool for ProjectDocs {
59    const NAME: &'static str = "project_docs";
60    type Error = DocsError;
61    type Args = ProjectDocsArgs;
62    type Output = String;
63
64    async fn definition(&self, _: String) -> ToolDefinition {
65        ToolDefinition {
66            name: "project_docs".to_string(),
67            description:
68                "Fetch project documentation for context. Types: readme, contributing, changelog, license, codeofconduct, agents (AGENTS.md/CLAUDE.md), context (readme + agent instructions - RECOMMENDED), all"
69                    .to_string(),
70            parameters: parameters_schema::<ProjectDocsArgs>(),
71        }
72    }
73
74    async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
75        let current_dir = std::env::current_dir().map_err(DocsError::from)?;
76        let max_chars = args.max_chars.min(20000);
77
78        let files_to_check = match args.doc_type {
79            DocType::Readme => vec![
80                "README.md",
81                "README.rst",
82                "README.txt",
83                "README",
84                "readme.md",
85            ],
86            DocType::Contributing => vec!["CONTRIBUTING.md", "CONTRIBUTING", "contributing.md"],
87            DocType::Changelog => vec![
88                "CHANGELOG.md",
89                "CHANGELOG",
90                "HISTORY.md",
91                "CHANGES.md",
92                "changelog.md",
93            ],
94            DocType::License => vec!["LICENSE", "LICENSE.md", "LICENSE.txt", "license"],
95            DocType::CodeOfConduct => vec!["CODE_OF_CONDUCT.md", "code_of_conduct.md"],
96            DocType::Agents => vec![
97                "AGENTS.md",
98                "CLAUDE.md",
99                ".github/copilot-instructions.md",
100                ".cursor/rules",
101                "CODING_GUIDELINES.md",
102            ],
103            DocType::Context => vec![
104                "README.md",
105                "AGENTS.md",
106                "CLAUDE.md",
107                ".github/copilot-instructions.md",
108            ],
109            DocType::All => vec![
110                "README.md",
111                "AGENTS.md",
112                "CLAUDE.md",
113                "CONTRIBUTING.md",
114                "CHANGELOG.md",
115                "CODE_OF_CONDUCT.md",
116            ],
117        };
118
119        let mut output = String::new();
120        let mut found_any = false;
121        // Track if we found an agent instructions file (AGENTS.md often symlinks to CLAUDE.md)
122        let mut found_agent_doc = false;
123
124        for filename in files_to_check {
125            // Skip CLAUDE.md if we already found AGENTS.md (avoid duplicate from symlink)
126            if filename == "CLAUDE.md" && found_agent_doc {
127                continue;
128            }
129
130            let path: PathBuf = current_dir.join(filename);
131            if path.exists() {
132                match tokio::fs::read_to_string(&path).await {
133                    Ok(content) => {
134                        found_any = true;
135
136                        // Mark that we found an agent doc file
137                        if filename == "AGENTS.md" {
138                            found_agent_doc = true;
139                        }
140
141                        output.push_str(&format!("=== {} ===\n", filename));
142
143                        // Truncate if too long (use char boundary-safe truncation)
144                        let char_count = content.chars().count();
145                        if char_count > max_chars {
146                            let truncated: String = content.chars().take(max_chars).collect();
147                            output.push_str(&truncated);
148                            output.push_str(&format!(
149                                "\n\n[... truncated, {} more chars ...]\n",
150                                char_count - max_chars
151                            ));
152                        } else {
153                            output.push_str(&content);
154                        }
155                        output.push_str("\n\n");
156
157                        // For single doc types, return after finding first match
158                        // Context and All gather multiple files
159                        if !matches!(args.doc_type, DocType::All | DocType::Context) {
160                            break;
161                        }
162                    }
163                    Err(e) => {
164                        output.push_str(&format!("Error reading {}: {}\n", filename, e));
165                    }
166                }
167            }
168        }
169
170        if !found_any {
171            output = format!(
172                "No {:?} documentation found in project root.",
173                args.doc_type
174            );
175        }
176
177        Ok(output)
178    }
179}