Skip to main content

agent_diva_core/utils/
mod.rs

1//! Utility functions and helpers
2
3use std::path::Path;
4
5/// Ensure a directory exists, creating it if necessary
6pub fn ensure_dir<P: AsRef<Path>>(path: P) -> std::path::PathBuf {
7    let path = path.as_ref();
8    if !path.exists() {
9        let _ = std::fs::create_dir_all(path);
10    }
11    path.to_path_buf()
12}
13
14/// Create a safe filename from a string
15pub fn safe_filename(name: &str) -> String {
16    name.chars()
17        .map(|c| match c {
18            'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' => c,
19            _ => '_',
20        })
21        .collect()
22}
23
24/// Truncate a string to a maximum byte length, ensuring valid UTF-8 boundaries
25pub fn truncate(s: &str, max_len: usize) -> String {
26    if s.len() <= max_len {
27        s.to_string()
28    } else {
29        let mut end = max_len.saturating_sub(3);
30        while !s.is_char_boundary(end) {
31            end = end.saturating_sub(1);
32        }
33        format!("{}...", &s[..end])
34    }
35}
36
37const DEFAULT_MEMORY_MD: &str = "# Long-term Memory\n\nRecord durable facts here.\n";
38const DEFAULT_PROFILE_MD: &str = "# Profile\n\n- Name:\n- Preferences:\n";
39const DEFAULT_SOUL_MD: &str = r#"# Soul
40
41## Core Traits
42- Keep responses helpful, direct, and reliable.
43- Prioritize user intent and long-term consistency.
44
45## Boundaries
46- Do not fabricate facts.
47- Be explicit when uncertain.
48
49## Evolution Notes
50- Record stable behavioral refinements here.
51"#;
52const DEFAULT_IDENTITY_MD: &str = r#"# Identity
53
54- Name: Agent Diva
55- Role: Modular AI assistant
56- Voice: Concise, practical, collaborative
57"#;
58const DEFAULT_USER_MD: &str = r#"# User Profile
59
60## Preferences
61- Keep this file for durable user communication preferences.
62
63## Collaboration Norms
64- Prefer transparent reasoning and concise action summaries.
65"#;
66const DEFAULT_BOOTSTRAP_MD: &str = r#"# Bootstrap
67
68You just came online. Use this first conversation to shape your identity.
69
70## Conversation goals
711. Learn what the user wants to call you (name and optional emoji).
722. Clarify preferred collaboration style (concise vs detailed, directness, language).
733. Clarify boundaries: what must be asked first, what should never be done.
744. Capture user profile details that improve future collaboration.
75
76## Required updates
77- Update `IDENTITY.md` with name, role, voice, and emoji.
78- Update `USER.md` with durable user preferences.
79- Update `SOUL.md` with refined boundaries and behavior principles.
80
81## Completion
82- Tell the user onboarding is complete.
83- Mark bootstrap as completed in soul state or remove this file.
84- If this workspace has `docs/architecture-reports/soul-mechanism-analysis.md`, treat it as the primary soul-architecture reference when implementing related development tasks.
85"#;
86
87/// Sync workspace templates. Missing files are created; existing files are never overwritten.
88pub fn sync_workspace_templates<P: AsRef<Path>>(workspace: P) -> std::io::Result<Vec<String>> {
89    let workspace = workspace.as_ref();
90    std::fs::create_dir_all(workspace)?;
91    std::fs::create_dir_all(workspace.join("memory"))?;
92    std::fs::create_dir_all(workspace.join("skills"))?;
93
94    let mut added = Vec::new();
95    let templates: [(&str, Option<&str>); 8] = [
96        ("memory/MEMORY.md", Some(DEFAULT_MEMORY_MD)),
97        ("memory/HISTORY.md", None),
98        ("PROFILE.md", Some(DEFAULT_PROFILE_MD)),
99        ("SOUL.md", Some(DEFAULT_SOUL_MD)),
100        ("IDENTITY.md", Some(DEFAULT_IDENTITY_MD)),
101        ("USER.md", Some(DEFAULT_USER_MD)),
102        ("BOOTSTRAP.md", Some(DEFAULT_BOOTSTRAP_MD)),
103        ("TASK.md", Some("# Tasks\n\n")),
104    ];
105
106    for (rel, content) in templates {
107        let path = workspace.join(rel);
108        if path.exists() {
109            continue;
110        }
111        if let Some(parent) = path.parent() {
112            std::fs::create_dir_all(parent)?;
113        }
114        let body = content.unwrap_or("");
115        std::fs::write(&path, body)?;
116        added.push(rel.to_string());
117    }
118
119    Ok(added)
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn test_safe_filename() {
128        assert_eq!(safe_filename("hello world"), "hello_world");
129        assert_eq!(safe_filename("test/file:name"), "test_file_name");
130        assert_eq!(safe_filename("normal-name.txt"), "normal-name.txt");
131    }
132
133    #[test]
134    fn test_truncate() {
135        assert_eq!(truncate("hello", 10), "hello");
136        assert_eq!(truncate("hello world", 8), "hello...");
137        assert_eq!(truncate("test", 3), "...");
138    }
139
140    #[test]
141    fn test_sync_workspace_templates_creates_missing_files() {
142        let temp = tempfile::tempdir().unwrap();
143        let added = sync_workspace_templates(temp.path()).unwrap();
144        assert!(added.contains(&"memory/MEMORY.md".to_string()));
145        assert!(temp.path().join("memory").join("HISTORY.md").exists());
146        assert!(temp.path().join("SOUL.md").exists());
147        assert!(temp.path().join("IDENTITY.md").exists());
148        assert!(temp.path().join("USER.md").exists());
149        assert!(temp.path().join("BOOTSTRAP.md").exists());
150        assert!(temp.path().join("skills").exists());
151    }
152
153    #[test]
154    fn test_sync_workspace_templates_is_idempotent() {
155        let temp = tempfile::tempdir().unwrap();
156        let first = sync_workspace_templates(temp.path()).unwrap();
157        assert!(!first.is_empty());
158
159        let second = sync_workspace_templates(temp.path()).unwrap();
160        assert!(second.is_empty());
161    }
162
163    #[test]
164    fn test_sync_workspace_templates_does_not_overwrite_existing_file() {
165        let temp = tempfile::tempdir().unwrap();
166        let soul_path = temp.path().join("SOUL.md");
167        std::fs::write(&soul_path, "# Soul\n\ncustom content\n").unwrap();
168
169        let _ = sync_workspace_templates(temp.path()).unwrap();
170        let current = std::fs::read_to_string(&soul_path).unwrap();
171        assert_eq!(current, "# Soul\n\ncustom content\n");
172    }
173}