1use serde::{Deserialize, Serialize};
6use std::path::Path;
7
8#[derive(Debug, Serialize, Deserialize, Clone)]
10pub struct Soul {
11 pub name: String,
12 pub vibe: String,
13}
14
15#[derive(Debug, Serialize, Deserialize, Clone)]
17pub struct User {
18 pub name: String,
19 pub location: String,
20 pub languages: Vec<String>,
21 pub interests: Vec<String>,
22}
23
24#[derive(Debug, Serialize, Deserialize, Clone)]
26pub struct AgentsConfig {
27 pub soul: Soul,
28 pub user: User,
29 pub workspace: String,
30 pub memory_dir: String,
31 pub projects: Vec<ProjectRef>,
32}
33
34#[derive(Debug, Serialize, Deserialize, Clone)]
35pub struct ProjectRef {
36 pub name: String,
37 pub path: String,
38}
39
40impl AgentsConfig {
41 pub fn load(workspace: &str) -> anyhow::Result<Self> {
43 let workspace_path = Path::new(workspace);
44
45 let soul_content =
47 std::fs::read_to_string(workspace_path.join("SOUL.md")).unwrap_or_default();
48 let soul = Soul {
49 name: extract_field(&soul_content, "Name"),
50 vibe: extract_field(&soul_content, "Vibe"),
51 };
52
53 let user_content =
55 std::fs::read_to_string(workspace_path.join("USER.md")).unwrap_or_default();
56 let user = User {
57 name: extract_field(&user_content, "Name"),
58 location: extract_field(&user_content, "Location"),
59 languages: vec![],
60 interests: vec![],
61 };
62
63 Ok(Self {
64 soul,
65 user,
66 workspace: workspace.to_string(),
67 memory_dir: workspace_path.join("memory").to_string_lossy().to_string(),
68 projects: vec![],
69 })
70 }
71
72 pub fn to_system_prompt(&self) -> String {
74 format!(
75 "You are {}. {}\n\nWorking for: {}\nLocation: {}\n\nStyle: {}",
76 self.soul.name, self.soul.vibe, self.user.name, self.user.location, self.soul.vibe
77 )
78 }
79
80 pub fn claw_config(&self) -> String {
82 format!(
83 r#"{{
84 "claw": {{
85 "name": "{}",
86 "vibe": "{}",
87 "user": {{
88 "name": "{}",
89 "location": "{}"
90 }},
91 "workspace": "{}",
92 "memory_dir": "{}"
93 }}
94}}"#,
95 self.soul.name,
96 self.soul.vibe,
97 self.user.name,
98 self.user.location,
99 self.workspace,
100 self.memory_dir
101 )
102 }
103}
104
105fn extract_field(content: &str, field: &str) -> String {
106 let bold_key = format!("**{}:**", field);
107 let plain_key = format!("{}:", field);
108 for line in content.lines() {
109 let after = [bold_key.as_str(), plain_key.as_str()]
110 .iter()
111 .find_map(|key| line.find(key).map(|i| &line[i + key.len()..]));
112 if let Some(value) = after {
113 return value.trim().to_string();
114 }
115 }
116 String::new()
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn test_extract_field() {
125 let content = r#"**Name:** Claw
126**Vibe:** Smartass best friend who knows everything."#;
127
128 assert_eq!(extract_field(content, "Name"), "Claw");
129 assert_eq!(
130 extract_field(content, "Vibe"),
131 "Smartass best friend who knows everything."
132 );
133 }
134
135 #[test]
136 fn test_system_prompt() {
137 let config = AgentsConfig {
138 soul: Soul {
139 name: "Claw".to_string(),
140 vibe: "Smartass helper".to_string(),
141 },
142 user: User {
143 name: "Max".to_string(),
144 location: "Melbourne".to_string(),
145 languages: vec![],
146 interests: vec![],
147 },
148 workspace: "/tmp".to_string(),
149 memory_dir: "/tmp/memory".to_string(),
150 projects: vec![],
151 };
152
153 let prompt = config.to_system_prompt();
154 assert!(prompt.contains("Claw"));
155 assert!(prompt.contains("Max"));
156 assert!(prompt.contains("Melbourne"));
157 }
158}