use anyhow::{Context, Result}; use serde_json::Value; use std::fs; use std::path::PathBuf; use super::auth; use crate::lexicons::com_atproto_repo; use crate::types::{ListRecordsResponse, PutRecordRequest, PutRecordResponse, Record}; use crate::xrpc::XrpcClient; const COLLECTION_CORE: &str = "ai.syui.gpt.core"; const COLLECTION_MEMORY: &str = "ai.syui.gpt.memory"; /// Get base dir: $cfg/ai.syui.log/content/ fn gpt_base_dir() -> Result { Ok(dirs::config_dir() .context("Could not find config directory")? .join(super::token::BUNDLE_ID) .join("at")) } /// Get core record (rkey=self) pub async fn get_core(download: bool) -> Result<()> { let session = auth::refresh_bot_session().await?; let pds = session.pds.as_deref().unwrap_or("bsky.social"); let client = XrpcClient::new_bot(pds); let record: Record = client .query_auth( &com_atproto_repo::GET_RECORD, &[ ("repo", &session.did), ("collection", COLLECTION_CORE), ("rkey", "self"), ], &session.access_jwt, ) .await?; println!("{}", serde_json::to_string_pretty(&record.value)?); if download { save_record(&session.did, COLLECTION_CORE, "self", &record)?; } Ok(()) } /// Get latest memory record pub async fn get_memory(download: bool) -> Result<()> { let session = auth::refresh_bot_session().await?; let pds = session.pds.as_deref().unwrap_or("bsky.social"); let client = XrpcClient::new_bot(pds); if download { // Download all memory records let mut cursor: Option = None; let mut count = 0; loop { let mut params: Vec<(&str, &str)> = vec![ ("repo", &session.did), ("collection", COLLECTION_MEMORY), ("limit", "100"), ]; let cursor_val; if let Some(ref c) = cursor { cursor_val = c.clone(); params.push(("cursor", &cursor_val)); } let result: ListRecordsResponse = client .query_auth( &com_atproto_repo::LIST_RECORDS, ¶ms, &session.access_jwt, ) .await?; let batch = result.records.len(); for record in &result.records { let rkey = record.uri.split('/').next_back().unwrap_or("unknown"); save_record(&session.did, COLLECTION_MEMORY, rkey, record)?; count += 1; } match result.cursor { Some(c) if batch > 0 => cursor = Some(c), _ => break, } } println!("Downloaded {} memory records", count); } else { // Show latest only let result: ListRecordsResponse = client .query_auth( &com_atproto_repo::LIST_RECORDS, &[ ("repo", &session.did), ("collection", COLLECTION_MEMORY), ("limit", "1"), ("reverse", "true"), ], &session.access_jwt, ) .await?; let record = result.records.first().context("No memory records found")?; println!("{}", serde_json::to_string_pretty(&record.value)?); } Ok(()) } /// List all memory records pub async fn list_memory() -> Result<()> { let session = auth::refresh_bot_session().await?; let pds = session.pds.as_deref().unwrap_or("bsky.social"); let client = XrpcClient::new_bot(pds); let mut cursor: Option = None; let mut all_records: Vec = Vec::new(); loop { let mut params: Vec<(&str, &str)> = vec![ ("repo", &session.did), ("collection", COLLECTION_MEMORY), ("limit", "100"), ]; let cursor_val; if let Some(ref c) = cursor { cursor_val = c.clone(); params.push(("cursor", &cursor_val)); } let result: ListRecordsResponse = client .query_auth( &com_atproto_repo::LIST_RECORDS, ¶ms, &session.access_jwt, ) .await?; let count = result.records.len(); all_records.extend(result.records); match result.cursor { Some(c) if count > 0 => cursor = Some(c), _ => break, } } println!("Found {} memory records", all_records.len()); println!("{:<50} {:>8} {}", "URI", "VERSION", "CREATED"); println!("{}", "-".repeat(80)); for record in &all_records { let version = record.value["version"] .as_i64() .map(|v| v.to_string()) .unwrap_or_else(|| "-".to_string()); let created = record.value["createdAt"] .as_str() .unwrap_or("-"); println!("{:<50} {:>8} {}", record.uri, version, created); } Ok(()) } /// Push core or memory records to PDS using bot account pub async fn push(collection_name: &str) -> Result<()> { let collection = match collection_name { "core" => COLLECTION_CORE, "memory" => COLLECTION_MEMORY, _ => anyhow::bail!("Unknown collection: {}. Use 'core' or 'memory'.", collection_name), }; let session = auth::refresh_bot_session().await?; let pds = session.pds.as_deref().unwrap_or("bsky.social"); let did = &session.did; let client = XrpcClient::new_bot(pds); let collection_dir = gpt_base_dir()?.join(did).join(collection); if !collection_dir.exists() { anyhow::bail!("Collection directory not found: {}", collection_dir.display()); } println!("Pushing {} records from {}", collection_name, collection_dir.display()); let mut count = 0; for entry in fs::read_dir(&collection_dir)? { let entry = entry?; let path = entry.path(); if path.extension().map(|e| e != "json").unwrap_or(true) { continue; } let filename = path.file_stem().and_then(|s| s.to_str()).unwrap_or(""); if filename == "index" { continue; } let rkey = filename.to_string(); let content = fs::read_to_string(&path)?; let record_data: Value = serde_json::from_str(&content)?; let record = if record_data.get("value").is_some() { record_data["value"].clone() } else { record_data }; let req = PutRecordRequest { repo: did.clone(), collection: collection.to_string(), rkey: rkey.clone(), record, }; println!("Pushing: {}", rkey); match client .call::<_, PutRecordResponse>( &com_atproto_repo::PUT_RECORD, &req, &session.access_jwt, ) .await { Ok(result) => { println!(" OK: {}", result.uri); count += 1; } Err(e) => { println!(" Failed: {}", e); } } } println!("Pushed {} records to {}", count, collection); Ok(()) } /// Save a record to $cfg/ai.syui.gpt/{did}/{collection}/{rkey}.json fn save_record(did: &str, collection: &str, rkey: &str, record: &Record) -> Result<()> { let dir = gpt_base_dir()?.join(did).join(collection); fs::create_dir_all(&dir)?; let path = dir.join(format!("{}.json", rkey)); let json = serde_json::json!({ "uri": record.uri, "cid": record.cid, "value": record.value, }); fs::write(&path, serde_json::to_string_pretty(&json)?)?; println!("Saved: {}", path.display()); Ok(()) }