1pub mod types;
17pub mod context;
18pub mod parse;
19pub mod im;
20pub mod evolution;
21pub mod evolution_cache;
22pub mod evolution_runtime;
23pub mod signal_rules;
24pub mod webhook;
25pub mod memory;
26pub mod knowledge;
27pub mod community;
28pub mod tasks;
29pub mod identity;
30pub mod files;
31pub mod daemon;
32pub mod ui;
33
34use reqwest::Client as HttpClient;
35use ed25519_dalek::{SigningKey, Signer};
36use sha2::{Sha256, Digest};
37
38fn resolve_api_key(explicit: &str) -> String {
44 if !explicit.is_empty() {
45 return explicit.to_string();
46 }
47 if let Ok(env_key) = std::env::var("PRISMER_API_KEY") {
48 if !env_key.is_empty() {
49 return env_key;
50 }
51 }
52 if let Some(home) = dirs::home_dir() {
53 let config_path = home.join(".prismer").join("config.toml");
54 if let Ok(raw) = std::fs::read_to_string(&config_path) {
55 if let Some(key) = toml_find(&raw, "api_key") {
56 return key;
57 }
58 }
59 }
60 String::new()
61}
62
63fn resolve_base_url(explicit: Option<&str>) -> String {
69 if let Some(url) = explicit {
70 if !url.is_empty() {
71 return url.to_string();
72 }
73 }
74 if let Ok(env_url) = std::env::var("PRISMER_BASE_URL") {
75 if !env_url.is_empty() {
76 return env_url;
77 }
78 }
79 if let Some(home) = dirs::home_dir() {
80 let config_path = home.join(".prismer").join("config.toml");
81 if let Ok(raw) = std::fs::read_to_string(&config_path) {
82 if let Some(url) = toml_find(&raw, "base_url") {
83 return url;
84 }
85 }
86 }
87 "https://prismer.cloud".to_string()
88}
89
90fn toml_find(haystack: &str, key: &str) -> Option<String> {
93 for line in haystack.lines() {
94 let trimmed = line.trim_start();
95 if trimmed.starts_with(key) {
96 let rest = &trimmed[key.len()..];
97 let rest = rest.trim_start_matches(|c: char| c.is_whitespace());
98 if let Some(rest) = rest.strip_prefix('=') {
99 let rest = rest.trim_start();
100 for quote in ['"', '\''] {
101 if rest.starts_with(quote) {
102 let inner = &rest[1..];
103 if let Some(end) = inner.find(quote) {
104 return Some(inner[..end].to_string());
105 }
106 }
107 }
108 }
109 }
110 }
111 None
112}
113
114pub struct PrismerClient {
116 http: HttpClient,
117 api_key: String,
118 base_url: String,
119 signing_key: Option<SigningKey>,
121 pub identity_did: Option<String>,
123}
124
125impl PrismerClient {
126 pub fn new(api_key: &str, base_url: Option<&str>) -> Self {
129 let resolved_key = resolve_api_key(api_key);
130 let resolved_url = resolve_base_url(base_url);
131 Self {
132 http: HttpClient::new(),
133 api_key: resolved_key,
134 base_url: resolved_url,
135 signing_key: None,
136 identity_did: None,
137 }
138 }
139
140 pub fn new_with_identity(api_key: &str, base_url: Option<&str>) -> Self {
144 let resolved_key = resolve_api_key(api_key);
145 let resolved_url = resolve_base_url(base_url);
146 let seed: [u8; 32] = Sha256::digest(resolved_key.as_bytes()).into();
147 let signing_key = SigningKey::from_bytes(&seed);
148 let pub_key = signing_key.verifying_key();
149 let did = public_key_to_did_key(&pub_key.to_bytes());
150 Self {
151 http: HttpClient::new(),
152 api_key: resolved_key,
153 base_url: resolved_url,
154 signing_key: Some(signing_key),
155 identity_did: Some(did),
156 }
157 }
158
159 pub(crate) fn sign_message(&self, content: &str, msg_type: &str) -> Option<(String, String, String, u64)> {
163 let key = self.signing_key.as_ref()?;
164 let did = self.identity_did.as_ref()?;
165 let content_hash = hex::encode(Sha256::digest(content.as_bytes()));
166 let timestamp = std::time::SystemTime::now()
167 .duration_since(std::time::UNIX_EPOCH).ok()?.as_millis() as u64;
168 let payload = format!("1|{}|{}|{}|{}", did, msg_type, timestamp, content_hash);
169 let sig = key.sign(payload.as_bytes());
170 Some((content_hash, base64::Engine::encode(&base64::engine::general_purpose::STANDARD, sig.to_bytes()), did.clone(), timestamp))
171 }
172
173 pub fn context(&self) -> context::ContextClient<'_> {
175 context::ContextClient { client: self }
176 }
177
178 pub fn parse(&self) -> parse::ParseClient<'_> {
180 parse::ParseClient { client: self }
181 }
182
183 pub fn im(&self) -> im::IMClient<'_> {
185 im::IMClient::new(self)
186 }
187
188 pub fn evolution(&self) -> evolution::EvolutionClient<'_> {
190 evolution::EvolutionClient { client: self }
191 }
192
193 pub fn memory(&self) -> memory::MemoryClient<'_> {
195 memory::MemoryClient { client: self }
196 }
197
198 pub fn knowledge(&self) -> knowledge::KnowledgeLinkClient<'_> {
200 knowledge::KnowledgeLinkClient { client: self }
201 }
202
203 pub fn community(&self) -> community::CommunityClient<'_> {
205 community::CommunityClient { client: self }
206 }
207
208 pub fn tasks(&self) -> tasks::TasksClient<'_> {
210 tasks::TasksClient { client: self }
211 }
212
213 pub fn identity(&self) -> identity::IdentityClient<'_> {
215 identity::IdentityClient { client: self }
216 }
217
218 pub fn files(&self) -> files::FilesClient<'_> {
220 files::FilesClient { client: self }
221 }
222
223 pub async fn request<T: serde::de::DeserializeOwned>(
225 &self,
226 method: reqwest::Method,
227 path: &str,
228 body: Option<serde_json::Value>,
229 ) -> Result<types::ApiResponse<T>, types::PrismerError> {
230 let url = format!("{}{}", self.base_url, path);
231 let mut req = self.http.request(method, &url)
232 .header("Authorization", format!("Bearer {}", self.api_key))
233 .header("Content-Type", "application/json");
234
235 if let Some(b) = body {
236 req = req.json(&b);
237 }
238
239 let resp = req.send().await.map_err(|e| types::PrismerError::Network(e.to_string()))?;
240 let status = resp.status();
241 let text = resp.text().await.map_err(|e| types::PrismerError::Network(e.to_string()))?;
242
243 if !status.is_success() {
244 return Err(types::PrismerError::Api {
245 status: status.as_u16(),
246 message: text,
247 });
248 }
249
250 serde_json::from_str(&text).map_err(|e| types::PrismerError::Parse(e.to_string()))
251 }
252}
253
254fn public_key_to_did_key(pub_key: &[u8; 32]) -> String {
256 let mut multicodec = vec![0xed, 0x01];
258 multicodec.extend_from_slice(pub_key);
259 format!("did:key:z{}", bs58::encode(&multicodec).into_string())
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 #[test]
267 fn client_default_base_url() {
268 let client = PrismerClient::new("sk-prismer-live-abc123", None);
269 assert_eq!(client.base_url, "https://prismer.cloud");
270 assert_eq!(client.api_key, "sk-prismer-live-abc123");
271 }
272
273 #[test]
274 fn client_custom_base_url() {
275 let client = PrismerClient::new("sk-test", Some("https://cloud.prismer.dev"));
276 assert_eq!(client.base_url, "https://cloud.prismer.dev");
277 }
278
279 #[test]
280 fn client_empty_api_key() {
281 let client = PrismerClient::new("", None);
282 assert_eq!(client.api_key, "");
283 }
284
285 #[test]
286 fn client_context_returns_context_client() {
287 let client = PrismerClient::new("sk-test", None);
288 let _ctx = client.context();
289 }
290
291 #[test]
292 fn client_parse_returns_parse_client() {
293 let client = PrismerClient::new("sk-test", None);
294 let _p = client.parse();
295 }
296
297 #[test]
298 fn client_im_returns_im_client() {
299 let client = PrismerClient::new("sk-test", None);
300 let _im = client.im();
301 }
302
303 #[test]
304 fn client_evolution_returns_evolution_client() {
305 let client = PrismerClient::new("sk-test", None);
306 let _ev = client.evolution();
307 }
308
309 #[test]
310 fn client_memory_returns_memory_client() {
311 let client = PrismerClient::new("sk-test", None);
312 let _m = client.memory();
313 }
314
315 #[test]
316 fn client_tasks_returns_tasks_client() {
317 let client = PrismerClient::new("sk-test", None);
318 let _t = client.tasks();
319 }
320
321 #[test]
322 fn client_identity_returns_identity_client() {
323 let client = PrismerClient::new("sk-test", None);
324 let _id = client.identity();
325 }
326
327 #[test]
328 fn client_files_returns_files_client() {
329 let client = PrismerClient::new("sk-test", None);
330 let _f = client.files();
331 }
332
333 #[test]
334 fn client_community_returns_community_client() {
335 let client = PrismerClient::new("sk-test", None);
336 let _c = client.community();
337 }
338
339 #[test]
340 fn client_knowledge_returns_knowledge_client() {
341 let client = PrismerClient::new("sk-test", None);
342 let _k = client.knowledge();
343 }
344}