monothink 0.4.5

A totally non-agentic coding assistant
/*
* This file is part of monothink.
*
* monothink is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Foundation,
* either version 3 of the License, or (at your option) any later version.
*
* monothink is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Wavething.
* If not, see <https://www.gnu.org/licenses/>.

*/

use rig::tool::{Tool, ToolContext};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::path::{Path, PathBuf};

#[derive(Deserialize, Serialize)]
pub struct ListDirArgs {
    path: PathBuf,
    // recursive: Option<bool>,
    start: Option<usize>,
}

#[derive(Deserialize, Serialize)]
pub struct ListDir {
    base_dir: PathBuf,
    #[serde(default)]
    max_results: Option<usize>,
}

// @todo check access rights (i.e. calculated path must be below base_dir)
impl ListDir {
    pub fn new(base_dir: &Path, max_results: Option<usize>) -> Self {
        Self {
            base_dir: base_dir.to_path_buf(),
            max_results,
        }
    }
}
impl Tool for ListDir {
    const NAME: &'static str = "list_dir";

    type Error = super::ToolError;
    type Args = ListDirArgs;
    type Output = super::DirectoryListing;

    async fn call(
        &self,
        _context: &mut ToolContext,
        args: Self::Args,
    ) -> Result<Self::Output, Self::Error> {
        let path = super::allowed_canonical_path(&args.path, &self.base_dir)?;
        // list_dir(&path, args.recursive.unwrap_or(false))
        let mut result = super::list_dir(&path, false)?;
        // @todo clip long lists but preserve total entries (add actual entries as info)
        // @todo this is brickle, because it also includes permission denied errors, we should log the error and go on
        result.total_entries = result.entries.len();
        if let Some(max_results) = self.max_results
            && result.entries.len() > max_results
        {
            result.entries.truncate(max_results);
            result.truncated = true;
        }

        Ok(result)
    }

    fn description(&self) -> String {
        format!(
            "List the files and directories in the given directory (relative to workspace root), \
        returning {} entries. Use an empty string \"\" for the workspace root. \
        Results include both files and subdirectories with their types.",
            match self.max_results {
                Some(val) => format!("at most {}", val),
                None => "all".to_string(),
            }
        )
    }

    fn parameters(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "Path to the directory relative to project root directory. \
                     Use an empty string for the root directory."
                },
                "max_results": {
                    "type": "integer",
                    "description": "Optional, limit the number of entries to return to at most the given value."
                },
                // "start": {
                //     "type": "integer",
                //     "description": "Optional, zero-based index of the first result to return. \
                //     Use this to paginate through results if a previous call was truncated."
                // },
            },
            "required": ["path"]
        })
    }

    fn map_error(&self, error: Self::Error) -> rig::tool::ToolExecutionError {
        rig::tool::ToolExecutionError::from_error(error)
    }
}

#[tokio::test]
async fn test_list_dir() {
    // fake test to just read the current dir
    let base_dir = std::env::current_dir().expect("Cannot get current directory");
    let ld = ListDir::new(&base_dir, Some(100));
    let result = ld
        .call(
            &mut ToolContext::default(),
            ListDirArgs {
                path: PathBuf::from(""),
                // recursive: Some(true),
                start: None,
            },
        )
        .await
        .unwrap();
    println!("Result: {:?}", result.total_entries);
}