nanocodex 0.1.0

A small, high-performance, headless Rust agents SDK
Documentation
use std::{
    fs::{self, File},
    io::{self, Read},
    path::{Path, PathBuf},
};

use crate::NanocodexError;

const MAX_PROJECT_INSTRUCTIONS_BYTES: usize = 32 * 1024;
const CANDIDATE_FILENAMES: [&str; 2] = ["AGENTS.override.md", "AGENTS.md"];

pub(super) fn load_project_instructions(
    workspace: &Path,
) -> Result<Option<String>, NanocodexError> {
    let root = find_project_root(workspace)?;
    let mut directories = workspace
        .ancestors()
        .take_while(|directory| *directory != root)
        .map(Path::to_path_buf)
        .collect::<Vec<_>>();
    directories.push(root);
    directories.reverse();

    let mut remaining = MAX_PROJECT_INSTRUCTIONS_BYTES;
    let mut documents = Vec::new();
    for directory in directories {
        if remaining == 0 {
            break;
        }
        let Some(path) = instruction_file(&directory)? else {
            continue;
        };
        let data = read_bounded(&path, remaining).map_err(|source| {
            NanocodexError::ReadProjectInstructions {
                path: path.clone(),
                source,
            }
        })?;
        remaining -= data.len();
        let text = String::from_utf8_lossy(&data).into_owned();
        if !text.trim().is_empty() {
            documents.push(text);
        }
    }

    Ok((!documents.is_empty()).then(|| documents.join("\n\n")))
}

fn find_project_root(workspace: &Path) -> Result<PathBuf, NanocodexError> {
    for directory in workspace.ancestors() {
        let marker = directory.join(".git");
        match fs::metadata(&marker) {
            Ok(_) => return Ok(directory.to_path_buf()),
            Err(source) if source.kind() == io::ErrorKind::NotFound => {}
            Err(source) => {
                return Err(NanocodexError::ReadProjectInstructions {
                    path: marker,
                    source,
                });
            }
        }
    }
    Ok(workspace.to_path_buf())
}

fn instruction_file(directory: &Path) -> Result<Option<PathBuf>, NanocodexError> {
    for filename in CANDIDATE_FILENAMES {
        let path = directory.join(filename);
        match fs::metadata(&path) {
            Ok(metadata) if metadata.is_file() => return Ok(Some(path)),
            Ok(_) => {}
            Err(source) if source.kind() == io::ErrorKind::NotFound => {}
            Err(source) => {
                return Err(NanocodexError::ReadProjectInstructions { path, source });
            }
        }
    }
    Ok(None)
}

fn read_bounded(path: &Path, limit: usize) -> io::Result<Vec<u8>> {
    let file = File::open(path)?;
    let mut data = Vec::with_capacity(limit.min(8 * 1024));
    file.take(limit as u64).read_to_end(&mut data)?;
    Ok(data)
}