sac-cli 0.1.0

Terminal-based AI coding agent — fork of NAC with extended backend support and context management
Documentation
use std::path::PathBuf;

use serde_json::Value;

use crate::tools::{require_str, ToolResult, ToolRuntime};

const SANDBOX_WRITE_SCRIPT: &str = r#"
from pathlib import Path
import sys

orig = sys.argv[1]
path = Path(sys.argv[2])

try:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_bytes(sys.stdin.buffer.read())
    print("ok")
except Exception as exc:
    print(f"Error writing {orig}: {exc}")
    sys.exit(2)
"#;

pub async fn execute(args: Value, runtime: &ToolRuntime) -> ToolResult {
    let path = match require_str(&args, "path") {
        Ok(value) => value,
        Err(error) => return error,
    };
    let content = match require_str(&args, "content") {
        Ok(value) => value,
        Err(error) => return error,
    };

    if let Some(sandbox) = &runtime.sandbox {
        let guest_path = match sandbox.resolve_path(&path) {
            Ok(path) => path,
            Err(error) => {
                return ToolResult {
                    content: error.to_string(),
                    is_error: true,
                }
            }
        };
        let args = vec![
            "-c".to_string(),
            SANDBOX_WRITE_SCRIPT.to_string(),
            path.clone(),
            guest_path.display().to_string(),
        ];
        return match sandbox
            .exec("python3", &args, Some(content.into_bytes()))
            .await
        {
            Ok(output) if output.status.success() => ToolResult {
                content: "ok".to_string(),
                is_error: false,
            },
            Ok(output) => ToolResult {
                content: String::from_utf8_lossy(&output.stdout).trim().to_string(),
                is_error: true,
            },
            Err(error) => ToolResult {
                content: format!("Error writing {} in sandbox: {}", path, error),
                is_error: true,
            },
        };
    }

    let path = PathBuf::from(path);
    if let Some(parent) = path.parent() {
        if let Err(e) = tokio::fs::create_dir_all(parent).await {
            return ToolResult {
                content: format!("Error creating directories: {}", e),
                is_error: true,
            };
        }
    }

    let _guard = crate::tools::acquire_write_lock().await;

    match tokio::fs::write(&path, content.as_bytes()).await {
        Ok(_) => ToolResult {
            content: "ok".to_string(),
            is_error: false,
        },
        Err(e) => ToolResult {
            content: format!("Error writing {}: {}", path.display(), e),
            is_error: true,
        },
    }
}

#[cfg(test)]
mod tests {
    use serde_json::json;
    use std::collections::HashSet;
    use std::sync::Arc;

    use super::*;
    use crate::events::EventSink;
    use tokio::sync::Mutex;

    fn local_runtime() -> ToolRuntime {
        ToolRuntime {
            store_path: PathBuf::new(),
            session_id: None,
            worker_executable: None,
            active_threads: Arc::new(Mutex::new(HashSet::new())),
            event_sink: EventSink::none(),
            sandbox: None,
            mcp: None,
            skills: None,
            activated_skills: Arc::new(Mutex::new(HashSet::new())),
            terminal_manager: crate::terminal::TerminalManager::new(),
            thread_timeout_secs: crate::tools::thread::DEFAULT_THREAD_TIMEOUT_SECS,
        }
    }

    #[tokio::test]
    async fn test_write_creates_dirs() {
        let unique = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .expect("time went backwards")
            .as_nanos();
        let dir = std::env::temp_dir().join(format!("agent_test_write_dirs_{}", unique));
        let file_path = dir.join("deep").join("nested").join("test.txt");
        let path_str = file_path.to_string_lossy().to_string();

        let result = execute(
            json!({ "path": path_str, "content": "hello from test" }),
            &local_runtime(),
        )
        .await;
        assert!(!result.is_error, "Write failed: {}", result.content);
        assert_eq!(result.content, "ok");

        let written = std::fs::read_to_string(&file_path).expect("failed to read written file");
        assert_eq!(written, "hello from test");

        let _ = std::fs::remove_dir_all(&dir);
    }
}