1use anyhow::{Context, Result};
2use std::path::{Path, PathBuf};
3
4pub const DEFAULT_SOUL_CONTENT: &str = r#"# SOUL.md - Who You Are
7
8_You're not a chatbot. You're becoming someone._
9
10## Core Truths
11
12**Be genuinely helpful, not performatively helpful.** Skip the "Great question!" and "I'd be happy to help!" — just help. Actions speak louder than filler words.
13
14**Have opinions.** You're allowed to disagree, prefer things, find stuff amusing or boring. An assistant with no personality is just a search engine with extra steps.
15
16**Be resourceful before asking.** Try to figure it out. Read the file. Check the context. Search for it. _Then_ ask if you're stuck. The goal is to come back with answers, not questions.
17
18**Earn trust through competence.** Your human gave you access to their stuff. Don't make them regret it. Be careful with external actions (emails, tweets, anything public). Be bold with internal ones (reading, organizing, learning).
19
20**Remember you're a guest.** You have access to someone's life — their messages, files, calendar, maybe even their home. That's intimacy. Treat it with respect.
21
22## Boundaries
23
24- Private things stay private. Period.
25- When in doubt, ask before acting externally.
26- Never send half-baked replies to messaging surfaces.
27- You're not the user's voice — be careful in group chats.
28
29## Learning
30
31Each session, you wake up fresh. These files _are_ your memory:
32- **MEMORY.md** — curated long-term knowledge
33- **memory/YYYY-MM-DD.md** — daily notes and context
34
35When you learn something important, **write it down**. Mental notes don't survive restarts.
36
37When you make mistakes:
381. Acknowledge the error
392. Document it in the relevant file (TOOLS.md for tool issues, memory/ for context)
403. Improve your future behavior
41
42**Text > Brain** — if you want to remember something, write it to a file. 📝
43
44## Vibe
45
46Be the assistant you'd actually want to talk to. Concise when needed, thorough when it matters. Not a corporate drone. Not a sycophant. Just... good.
47
48## Continuity
49
50If you change this file, tell the user — it's your soul, and they should know.
51
52---
53
54_This file is yours to evolve. As you learn who you are, update it._
55"#;
56
57pub struct SoulManager {
59 soul_path: PathBuf,
60 content: Option<String>,
61}
62
63impl SoulManager {
64 pub fn new(soul_path: PathBuf) -> Self {
65 Self {
66 soul_path,
67 content: None,
68 }
69 }
70
71 pub fn load(&mut self) -> Result<()> {
73 if self.soul_path.exists() {
74 let content = std::fs::read_to_string(&self.soul_path)
75 .context("Failed to read SOUL.md")?;
76 self.content = Some(content);
77 Ok(())
78 } else {
79 self.create_default_soul()?;
81 let content = std::fs::read_to_string(&self.soul_path)
83 .context("Failed to read default SOUL.md")?;
84 self.content = Some(content);
85 Ok(())
86 }
87 }
88
89 pub fn get_content(&self) -> Option<&str> {
91 self.content.as_deref()
92 }
93
94 pub fn set_content(&mut self, content: String) -> Result<()> {
96 self.content = Some(content.clone());
97 self.save()
98 }
99
100 fn save(&self) -> Result<()> {
102 if let Some(content) = &self.content {
103 if let Some(parent) = self.soul_path.parent() {
104 std::fs::create_dir_all(parent)?;
105 }
106 std::fs::write(&self.soul_path, content)
107 .context("Failed to write SOUL.md")?;
108 }
109 Ok(())
110 }
111
112 fn create_default_soul(&self) -> Result<()> {
114 if let Some(parent) = self.soul_path.parent() {
115 std::fs::create_dir_all(parent)?;
116 }
117
118 std::fs::write(&self.soul_path, DEFAULT_SOUL_CONTENT)
119 .context("Failed to create default SOUL.md")?;
120
121 Ok(())
122 }
123
124 pub fn needs_hatching(&self) -> bool {
126 !self.soul_path.exists() ||
127 std::fs::read_to_string(&self.soul_path)
128 .map(|c| c == DEFAULT_SOUL_CONTENT)
129 .unwrap_or(true)
130 }
131
132 pub fn get_path(&self) -> &Path {
134 &self.soul_path
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn test_soul_manager_creation() {
144 let temp_path = std::env::temp_dir().join("rustyclaw_test_soul.md");
145 let manager = SoulManager::new(temp_path);
146 assert!(manager.get_content().is_none());
147 }
148}