diff --git a/src/core/config.rs b/src/core/config.rs index 684ca10..87bb52a 100644 --- a/src/core/config.rs +++ b/src/core/config.rs @@ -6,6 +6,8 @@ use std::path::PathBuf; use chrono::Utc; pub const DEFAULT_MEMORY: u64 = 100; +pub const COLLECTION_CORE: &str = "ai.syui.gpt.core"; +pub const COLLECTION_MEMORY: &str = "ai.syui.gpt.memory"; pub struct Config { pub path: Option, @@ -14,6 +16,20 @@ pub struct Config { pub memory: u64, } +impl Config { + pub fn did(&self) -> &str { + self.did.as_deref().unwrap_or("self") + } + + pub fn handle(&self) -> &str { + self.handle.as_deref().unwrap_or("self") + } + + pub fn identity(&self) -> &str { + if cfg!(windows) { self.handle() } else { self.did() } + } +} + #[derive(Debug, Deserialize)] struct ConfigFile { bot: Option, @@ -84,22 +100,20 @@ pub fn init() { } let cfg = load(); - let core_path = record_path(&cfg, "ai.syui.gpt.core", "self"); + let core_path = record_path(&cfg, COLLECTION_CORE, "self"); if !core_path.exists() { if let Some(parent) = core_path.parent() { let _ = fs::create_dir_all(parent); } - let did = cfg.did.clone().unwrap_or_else(|| "self".to_string()); - let handle = cfg.handle.clone().unwrap_or_else(|| "self".to_string()); let now = Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string(); let core_record = json!({ - "uri": format!("at://{}/ai.syui.gpt.core/self", did), + "uri": format!("at://{}/{}/self", cfg.did(), COLLECTION_CORE), "value": { - "$type": "ai.syui.gpt.core", - "did": did, - "handle": handle, + "$type": COLLECTION_CORE, + "did": cfg.did(), + "handle": cfg.handle(), "content": { - "$type": "ai.syui.gpt.core#markdown", + "$type": format!("{}#markdown", COLLECTION_CORE), "text": "" }, "createdAt": now @@ -108,7 +122,7 @@ pub fn init() { let _ = fs::write(&core_path, serde_json::to_string_pretty(&core_record).unwrap()); } - let memory_dir = collection_dir(&cfg, "ai.syui.gpt.memory"); + let memory_dir = collection_dir(&cfg, COLLECTION_MEMORY); let _ = fs::create_dir_all(&memory_dir); } @@ -121,23 +135,15 @@ pub fn base_dir(cfg: &Config) -> PathBuf { } } -pub fn identity(cfg: &Config) -> String { - if cfg!(windows) { - cfg.handle.clone().unwrap_or_else(|| "self".to_string()) - } else { - cfg.did.clone().unwrap_or_else(|| "self".to_string()) - } -} - /// $cfg/{did|handle}/{collection}/{rkey}.json pub fn record_path(cfg: &Config, collection: &str, rkey: &str) -> PathBuf { base_dir(cfg) - .join(identity(cfg)) + .join(cfg.identity()) .join(collection) .join(format!("{}.json", rkey)) } /// $cfg/{did|handle}/{collection}/ pub fn collection_dir(cfg: &Config, collection: &str) -> PathBuf { - base_dir(cfg).join(identity(cfg)).join(collection) + base_dir(cfg).join(cfg.identity()).join(collection) } diff --git a/src/core/reader.rs b/src/core/reader.rs index 2f2e7ce..9e60dc1 100644 --- a/src/core/reader.rs +++ b/src/core/reader.rs @@ -2,11 +2,11 @@ use anyhow::{Context, Result}; use serde_json::Value; use std::fs; -use crate::core::config; +use crate::core::config::{self, COLLECTION_CORE, COLLECTION_MEMORY}; pub fn read_core() -> Result { let cfg = config::load(); - let path = config::record_path(&cfg, "ai.syui.gpt.core", "self"); + let path = config::record_path(&cfg, COLLECTION_CORE, "self"); let content = fs::read_to_string(&path) .with_context(|| format!("Failed to read {}", path.display()))?; let record: Value = serde_json::from_str(&content) @@ -16,17 +16,19 @@ pub fn read_core() -> Result { pub fn read_memory_all() -> Result> { let cfg = config::load(); - let dir = config::collection_dir(&cfg, "ai.syui.gpt.memory"); - if !dir.exists() { - return Ok(Vec::new()); - } - let mut files: Vec<_> = fs::read_dir(&dir)? + let dir = config::collection_dir(&cfg, COLLECTION_MEMORY); + let entries = match fs::read_dir(&dir) { + Ok(entries) => entries, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()), + Err(e) => return Err(e).with_context(|| format!("Failed to read {}", dir.display())), + }; + let mut files: Vec<_> = entries .filter_map(|e| e.ok()) .filter(|e| e.path().extension().is_some_and(|ext| ext == "json")) .collect(); files.sort_by_key(|e| e.file_name()); - let mut records = Vec::new(); + let mut records = Vec::with_capacity(files.len()); for entry in &files { let path = entry.path(); let content = fs::read_to_string(&path) @@ -40,7 +42,7 @@ pub fn read_memory_all() -> Result> { pub fn memory_count() -> usize { let cfg = config::load(); - let dir = config::collection_dir(&cfg, "ai.syui.gpt.memory"); + let dir = config::collection_dir(&cfg, COLLECTION_MEMORY); fs::read_dir(&dir) .map(|entries| { entries diff --git a/src/core/writer.rs b/src/core/writer.rs index 95bf906..8c7b05e 100644 --- a/src/core/writer.rs +++ b/src/core/writer.rs @@ -1,20 +1,22 @@ use anyhow::{Context, Result}; use chrono::Utc; -use serde_json::json; +use serde_json::{json, Value}; use std::fs; +use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; -use crate::core::config; +use crate::core::config::{self, COLLECTION_MEMORY}; + +static TID_COUNTER: AtomicU64 = AtomicU64::new(0); fn generate_tid() -> String { - // ATProto TID: 64-bit integer as 13 base32-sortable chars - // bit 63: always 0 (sign), bits 62..10: timestamp (microseconds), bits 9..0: clock_id const CHARSET: &[u8] = b"234567abcdefghijklmnopqrstuvwxyz"; let micros = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_micros() as u64; - let v = (micros << 10) & 0x7FFFFFFFFFFFFFFF; + let clock_id = TID_COUNTER.fetch_add(1, Ordering::Relaxed) & 0x3FF; + let v = ((micros << 10) | clock_id) & 0x7FFFFFFFFFFFFFFF; let mut tid = String::with_capacity(13); for i in (0..13).rev() { let idx = ((v >> (i * 5)) & 0x1f) as usize; @@ -23,27 +25,29 @@ fn generate_tid() -> String { tid } -/// Save a single memory element as a new TID file -pub fn save_memory(content: &str) -> Result<()> { - let cfg = config::load(); - let did = cfg.did.clone().unwrap_or_else(|| "self".to_string()); - let tid = generate_tid(); +fn build_memory_record(did: &str, tid: &str, text: &str) -> Value { let now = Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string(); - - let record = json!({ - "uri": format!("at://{}/ai.syui.gpt.memory/{}", did, tid), + json!({ + "uri": format!("at://{}/{}/{}", did, COLLECTION_MEMORY, tid), "value": { - "$type": "ai.syui.gpt.memory", + "$type": COLLECTION_MEMORY, "did": did, "content": { - "$type": "ai.syui.gpt.memory#markdown", - "text": content + "$type": format!("{}#markdown", COLLECTION_MEMORY), + "text": text }, "createdAt": now } - }); + }) +} - let dir = config::collection_dir(&cfg, "ai.syui.gpt.memory"); +/// Save a single memory element as a new TID file +pub fn save_memory(content: &str) -> Result<()> { + let cfg = config::load(); + let tid = generate_tid(); + let record = build_memory_record(cfg.did(), &tid, content); + + let dir = config::collection_dir(&cfg, COLLECTION_MEMORY); fs::create_dir_all(&dir) .with_context(|| format!("Failed to create {}", dir.display()))?; let path = dir.join(format!("{}.json", tid)); @@ -55,15 +59,13 @@ pub fn save_memory(content: &str) -> Result<()> { /// Delete all memory files, then write new ones from the given items pub fn compress_memory(items: &[String]) -> Result<()> { let cfg = config::load(); - let did = cfg.did.clone().unwrap_or_else(|| "self".to_string()); - let dir = config::collection_dir(&cfg, "ai.syui.gpt.memory"); + let dir = config::collection_dir(&cfg, COLLECTION_MEMORY); // delete all existing memory files - if dir.exists() { - for entry in fs::read_dir(&dir)? { - let entry = entry?; + if let Ok(entries) = fs::read_dir(&dir) { + for entry in entries.flatten() { if entry.path().extension().is_some_and(|ext| ext == "json") { - fs::remove_file(entry.path())?; + let _ = fs::remove_file(entry.path()); } } } @@ -71,37 +73,9 @@ pub fn compress_memory(items: &[String]) -> Result<()> { fs::create_dir_all(&dir) .with_context(|| format!("Failed to create {}", dir.display()))?; - // write each item as a new TID file for item in items { - let micros = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_micros() as u64; - // small delay to ensure unique TIDs - std::thread::sleep(std::time::Duration::from_micros(1)); - - let v = (micros << 10) & 0x7FFFFFFFFFFFFFFF; - const CHARSET: &[u8] = b"234567abcdefghijklmnopqrstuvwxyz"; - let mut tid = String::with_capacity(13); - for i in (0..13).rev() { - let idx = ((v >> (i * 5)) & 0x1f) as usize; - tid.push(CHARSET[idx] as char); - } - - let now = Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string(); - let record = json!({ - "uri": format!("at://{}/ai.syui.gpt.memory/{}", did, tid), - "value": { - "$type": "ai.syui.gpt.memory", - "did": did, - "content": { - "$type": "ai.syui.gpt.memory#markdown", - "text": item - }, - "createdAt": now - } - }); - + let tid = generate_tid(); + let record = build_memory_record(cfg.did(), &tid, item); let path = dir.join(format!("{}.json", tid)); let json_str = serde_json::to_string_pretty(&record)?; fs::write(&path, json_str) diff --git a/src/main.rs b/src/main.rs index 2ff69c2..a7e6e11 100644 --- a/src/main.rs +++ b/src/main.rs @@ -167,21 +167,18 @@ fn is_command_available(cmd: &str) -> bool { fn print_status() { let cfg = config::load(); - let did = cfg.did.clone().unwrap_or_else(|| "self".to_string()); - let handle = cfg.handle.clone().unwrap_or_else(|| "self".to_string()); let base = config::base_dir(&cfg); - let id = config::identity(&cfg); let count = reader::memory_count(); println!("aigpt - AI memory MCP server\n"); println!("config: {}", config::config_file().display()); - println!("did: {}", did); - println!("handle: {}", handle); + println!("did: {}", cfg.did()); + println!("handle: {}", cfg.handle()); println!("memory: {}", cfg.memory); println!(); println!("path: {}/", base.display()); - println!(" {}/{}", id, "ai.syui.gpt.core/self.json"); - println!(" {}/{}", id, "ai.syui.gpt.memory/*.json"); + println!(" {}/{}/self.json", cfg.identity(), config::COLLECTION_CORE); + println!(" {}/{}/*.json", cfg.identity(), config::COLLECTION_MEMORY); println!(); println!("records: {}/{}", count, cfg.memory); } diff --git a/src/mcp/server.rs b/src/mcp/server.rs index 845ef30..663c4f2 100644 --- a/src/mcp/server.rs +++ b/src/mcp/server.rs @@ -72,7 +72,7 @@ impl MCPServer { }, "serverInfo": { "name": "aigpt", - "version": "0.3.0" + "version": env!("CARGO_PKG_VERSION") }, "instructions": instructions }