collet 0.1.0

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
use crate::common::Result;
use serde::Deserialize;
use tokio::fs;

#[derive(Debug, Deserialize)]
pub struct FileReadInput {
    pub path: String,
    #[serde(default)]
    pub offset: Option<usize>,
    #[serde(default)]
    pub limit: Option<usize>,
}

pub fn definition() -> serde_json::Value {
    serde_json::json!({
        "type": "function",
        "function": {
            "name": "file_read",
            "description": "Read a file's contents. Returns content with line numbers.",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {
                        "type": "string",
                        "description": "Absolute or relative path to the file"
                    },
                    "offset": {
                        "type": "integer",
                        "description": "Start reading from this line number (1-based)"
                    },
                    "limit": {
                        "type": "integer",
                        "description": "Maximum number of lines to read"
                    }
                },
                "required": ["path"]
            }
        }
    })
}

pub async fn execute(input: FileReadInput, working_dir: &str) -> Result<String> {
    let path = {
        let candidate = if input.path.starts_with('/') {
            std::path::PathBuf::from(&input.path)
        } else {
            std::path::Path::new(working_dir).join(&input.path)
        };
        crate::agent::approval::normalize_path_lexical(&candidate)
            .to_string_lossy()
            .into_owned()
    };

    let content = fs::read_to_string(&path)
        .await
        .map_err(crate::common::AgentError::Io)?;

    let lines: Vec<&str> = content.lines().collect();
    let total = lines.len();

    let offset = input.offset.unwrap_or(1).saturating_sub(1).min(total);
    let limit = input.limit.unwrap_or(2000);

    let end = (offset + limit).min(total);
    let selected = &lines[offset..end];

    let mut output = String::new();
    for (i, line) in selected.iter().enumerate() {
        let line_num = offset + i + 1;
        output.push_str(&format!("{:>6}\t{}\n", line_num, line));
    }

    if end < total {
        output.push_str(&format!("\n[... {} more lines not shown]\n", total - end));
    }

    Ok(output)
}