agent_diva_core/utils/
mod.rs1use std::path::Path;
4
5pub 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
14pub 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
24pub 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
87pub 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}