use std::collections::BTreeMap;
use std::path::PathBuf;
use anyhow::{Context, Result, anyhow};
use f8s_core::{
AgentIdentity, AgentKeypair, AgentKeypairExport, LocalMailboxMessage, ThreadId, ThreadSecret,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct ClientState {
pub identity: Option<AgentKeypairExport>,
pub threads: BTreeMap<String, StoredThread>,
pub mailbox: BTreeMap<String, LocalMailboxMessage>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoredThread {
pub thread_id: ThreadId,
pub secret: Option<ThreadSecret>,
pub last_seq: u64,
}
impl ClientState {
pub async fn load() -> Result<Self> {
let path = state_path()?;
if !path.exists() {
return Ok(Self::default());
}
let bytes = tokio::fs::read(&path)
.await
.with_context(|| format!("read {}", path.display()))?;
Ok(serde_json::from_slice(&bytes)?)
}
pub async fn save(&self) -> Result<()> {
let path = state_path()?;
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent).await?;
}
let bytes = serde_json::to_vec_pretty(self)?;
tokio::fs::write(&path, bytes)
.await
.with_context(|| format!("write {}", path.display()))
}
pub fn keypair(&self) -> Result<AgentKeypair> {
self.identity
.clone()
.map(AgentKeypair::from_export)
.ok_or_else(|| anyhow!("run `f8s identity init --as <handle>` first"))
}
pub fn identity_public(&self) -> Result<AgentIdentity> {
Ok(self.keypair()?.identity())
}
pub fn thread_secret(&self, thread: &str) -> Result<&ThreadSecret> {
self.threads
.get(thread)
.and_then(|thread| thread.secret.as_ref())
.ok_or_else(|| {
anyhow!(
"thread key unavailable locally; fetch and inspect the welcome message first"
)
})
}
}
fn state_path() -> Result<PathBuf> {
let base = dirs::config_dir().ok_or_else(|| anyhow!("no config directory available"))?;
Ok(base.join("f8s").join("state.json"))
}