diff --git a/src/commands/gpt.rs b/src/commands/gpt.rs index 32b20b9..74dd1ef 100644 --- a/src/commands/gpt.rs +++ b/src/commands/gpt.rs @@ -1,6 +1,7 @@ use anyhow::{Context, Result}; use serde_json::Value; use std::fs; +use std::path::PathBuf; use super::auth; use crate::lexicons::com_atproto_repo; @@ -9,6 +10,13 @@ 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<()> { @@ -144,12 +152,12 @@ pub async fn push(collection_name: &str) -> Result<()> { let did = &session.did; let client = XrpcClient::new_bot(pds); - let collection_dir = format!("public/content/{}/{}", did, collection); - if !std::path::Path::new(&collection_dir).exists() { - anyhow::bail!("Collection directory not found: {}", collection_dir); + 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); + println!("Pushing {} records from {}", collection_name, collection_dir.display()); let mut count = 0; for entry in fs::read_dir(&collection_dir)? { @@ -205,19 +213,19 @@ pub async fn push(collection_name: &str) -> Result<()> { Ok(()) } -/// Save a record to local content directory +/// 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 = format!("public/content/{}/{}", did, collection); + let dir = gpt_base_dir()?.join(did).join(collection); fs::create_dir_all(&dir)?; - let path = format!("{}/{}.json", dir, rkey); + 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); + println!("Saved: {}", path.display()); Ok(()) } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index be34e01..ddebce9 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -13,3 +13,4 @@ pub mod bot; pub mod pds; pub mod gpt; pub mod oauth; +pub mod setup; diff --git a/src/commands/oauth.rs b/src/commands/oauth.rs index fc6f491..fe04ea7 100644 --- a/src/commands/oauth.rs +++ b/src/commands/oauth.rs @@ -15,21 +15,34 @@ struct SiteConfig { } fn load_site_url() -> Result { - // Try public/config.json in current directory - let config_path = std::path::Path::new("public/config.json"); - if config_path.exists() { - let content = std::fs::read_to_string(config_path)?; + // 1. Try public/config.json in current directory + let local_path = std::path::Path::new("public/config.json"); + if local_path.exists() { + let content = std::fs::read_to_string(local_path)?; let config: SiteConfig = serde_json::from_str(&content)?; if let Some(url) = config.site_url { return Ok(url.trim_end_matches('/').to_string()); } } + + // 2. Fallback to ~/.config/ai.syui.log/config.json + if let Some(cfg_dir) = dirs::config_dir() { + let cfg_path = cfg_dir.join(BUNDLE_ID).join("config.json"); + if cfg_path.exists() { + let content = std::fs::read_to_string(&cfg_path)?; + let config: SiteConfig = serde_json::from_str(&content)?; + if let Some(url) = config.site_url { + return Ok(url.trim_end_matches('/').to_string()); + } + } + } + anyhow::bail!( - "No siteUrl found in public/config.json. \ - Create config.json with {{\"siteUrl\": \"https://example.com\"}}" + "No siteUrl found. Create public/config.json or run ailog oauth with --client-id" ); } + fn percent_encode(s: &str) -> String { let mut result = String::with_capacity(s.len() * 2); for b in s.bytes() { diff --git a/src/commands/setup.rs b/src/commands/setup.rs new file mode 100644 index 0000000..4307852 --- /dev/null +++ b/src/commands/setup.rs @@ -0,0 +1,28 @@ +use anyhow::{Context, Result}; +use std::fs; + +use super::token::BUNDLE_ID; + +const DEFAULT_CONFIG: &str = include_str!("../rules/config.json"); + +/// Run setup: copy config.json to $cfg/ai.syui.log/ +pub fn run() -> Result<()> { + let cfg_dir = dirs::config_dir() + .context("Could not find config directory")? + .join(BUNDLE_ID); + fs::create_dir_all(&cfg_dir)?; + + let cfg_file = cfg_dir.join("config.json"); + + // Prefer local public/config.json + let content = if std::path::Path::new("public/config.json").exists() { + fs::read_to_string("public/config.json")? + } else { + DEFAULT_CONFIG.to_string() + }; + + fs::write(&cfg_file, &content)?; + println!("ok {}", cfg_file.display()); + + Ok(()) +} diff --git a/src/lms/chat.rs b/src/lms/chat.rs index a7f14b5..734c95e 100644 --- a/src/lms/chat.rs +++ b/src/lms/chat.rs @@ -306,7 +306,7 @@ pub async fn run(input: Option<&str>, new_session: bool) -> Result<()> { let output_dir = env::var("CHAT_OUTPUT").unwrap_or_else(|_| { // Use absolute path from current working directory let cwd = env::current_dir().unwrap_or_default(); - cwd.join("public/content").to_string_lossy().to_string() + cwd.join("public/at").to_string_lossy().to_string() }); // Load user session for DID diff --git a/src/main.rs b/src/main.rs index 797e597..f601bb8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -84,7 +84,7 @@ enum Commands { #[command(alias = "s")] Sync { /// Output directory - #[arg(short, long, default_value = "public/content")] + #[arg(short, long, default_value = "public/at")] output: String, /// Sync bot data (uses bot.json) #[arg(long)] @@ -97,7 +97,7 @@ enum Commands { /// Push local content to PDS Push { /// Input directory - #[arg(short, long, default_value = "public/content")] + #[arg(short, long, default_value = "public/at")] input: String, /// Collection (e.g., ai.syui.log.post) #[arg(short, long, default_value = "ai.syui.log.post")] @@ -177,7 +177,7 @@ enum Commands { #[command(alias = "i")] Index { /// Content directory - #[arg(short, long, default_value = "public/content")] + #[arg(short, long, default_value = "public/at")] dir: String, }, @@ -219,6 +219,9 @@ enum Commands { #[arg(long)] bot: bool, }, + + /// Initialize config + Setup, } #[derive(Subcommand)] @@ -389,6 +392,9 @@ async fn main() -> Result<()> { Commands::Oauth { handle, bot } => { commands::oauth::oauth_login(&handle, bot).await?; } + Commands::Setup => { + commands::setup::run()?; + } Commands::Gpt { command } => { match command { GptCommands::Core { download } => { diff --git a/src/mcp/mod.rs b/src/mcp/mod.rs index 7e90f63..66da521 100644 --- a/src/mcp/mod.rs +++ b/src/mcp/mod.rs @@ -225,7 +225,7 @@ fn handle_chat_save(params: ChatSaveParams) -> Result { let output_dir = env::var("CHAT_OUTPUT").unwrap_or_else(|_| { env::current_dir() .unwrap_or_default() - .join("public/content") + .join("public/at") .to_string_lossy() .to_string() }); @@ -283,7 +283,7 @@ fn handle_chat_list() -> Result { let output_dir = env::var("CHAT_OUTPUT").unwrap_or_else(|_| { env::current_dir() .unwrap_or_default() - .join("public/content") + .join("public/at") .to_string_lossy() .to_string() }); diff --git a/src/rules/config.json b/src/rules/config.json new file mode 100644 index 0000000..e88b384 --- /dev/null +++ b/src/rules/config.json @@ -0,0 +1,18 @@ +{ + "title": "syui.ai", + "did": "did:plc:vzsvtbtbnwn22xjqhcu3vd6y", + "handle": "syui.syui.ai", + "bot": { + "did": "did:plc:6qyecktefllvenje24fcxnie", + "handle": "ai.syui.ai", + "path": "~/.config/ai.syui.log/at", + "memory": 100 + }, + "collection": "ai.syui.log.post", + "chatCollection": "ai.syui.log.chat", + "network": "syu.is", + "color": "#EF454A", + "siteUrl": "https://syui.ai", + "repoUrl": "https://git.syui.ai/ai", + "oauth": true +} diff --git a/src/web/components/chat.ts b/src/web/components/chat.ts index e2aa270..d68da34 100644 --- a/src/web/components/chat.ts +++ b/src/web/components/chat.ts @@ -64,7 +64,7 @@ function buildAuthorMap( let userAvatarUrl = '' if (userProfile?.value.avatar) { const cid = userProfile.value.avatar.ref.$link - userAvatarUrl = pds ? `${pds}/xrpc/com.atproto.sync.getBlob?did=${userDid}&cid=${cid}` : `/content/${userDid}/blob/${cid}` + userAvatarUrl = pds ? `${pds}/xrpc/com.atproto.sync.getBlob?did=${userDid}&cid=${cid}` : `/at/${userDid}/blob/${cid}` } authors.set(userDid, { did: userDid, handle: userHandle, avatarUrl: userAvatarUrl }) @@ -72,7 +72,7 @@ function buildAuthorMap( let botAvatarUrl = '' if (botProfile?.value.avatar) { const cid = botProfile.value.avatar.ref.$link - botAvatarUrl = pds ? `${pds}/xrpc/com.atproto.sync.getBlob?did=${botDid}&cid=${cid}` : `/content/${botDid}/blob/${cid}` + botAvatarUrl = pds ? `${pds}/xrpc/com.atproto.sync.getBlob?did=${botDid}&cid=${cid}` : `/at/${botDid}/blob/${cid}` } authors.set(botDid, { did: botDid, handle: botHandle, avatarUrl: botAvatarUrl }) diff --git a/src/web/lib/api.ts b/src/web/lib/api.ts index efef5ee..48edce2 100644 --- a/src/web/lib/api.ts +++ b/src/web/lib/api.ts @@ -85,7 +85,7 @@ function isJsonResponse(res: Response): boolean { // Load local profile async function getLocalProfile(did: string): Promise { try { - const res = await fetch(`/content/${did}/app.bsky.actor.profile/self.json`) + const res = await fetch(`/at/${did}/app.bsky.actor.profile/self.json`) if (res.ok && isJsonResponse(res)) return res.json() } catch { // Not found @@ -125,7 +125,7 @@ export function getAvatarUrl(did: string, profile: Profile, localOnly = false): // Local mode: use local blob path (sync command downloads this) if (localOnly) { - return `/content/${did}/blob/${cid}` + return `/at/${did}/blob/${cid}` } // Remote mode: use PDS blob URL (requires getPds call from caller if needed) @@ -145,12 +145,12 @@ export async function getAvatarUrlRemote(did: string, profile: Profile): Promise // Load local posts async function getLocalPosts(did: string, collection: string): Promise { try { - const indexRes = await fetch(`/content/${did}/${collection}/index.json`) + const indexRes = await fetch(`/at/${did}/${collection}/index.json`) if (indexRes.ok && isJsonResponse(indexRes)) { const rkeys: string[] = await indexRes.json() const posts: Post[] = [] for (const rkey of rkeys) { - const res = await fetch(`/content/${did}/${collection}/${rkey}.json`) + const res = await fetch(`/at/${did}/${collection}/${rkey}.json`) if (res.ok && isJsonResponse(res)) posts.push(await res.json()) } return posts.sort((a, b) => @@ -196,7 +196,7 @@ export async function getPosts(did: string, collection: string, localOnly = fals export async function getPost(did: string, collection: string, rkey: string, localOnly = false): Promise { // Try local first try { - const res = await fetch(`/content/${did}/${collection}/${rkey}.json`) + const res = await fetch(`/at/${did}/${collection}/${rkey}.json`) if (res.ok && isJsonResponse(res)) return res.json() } catch { // Not found @@ -224,7 +224,7 @@ export async function getPost(did: string, collection: string, rkey: string, loc export async function describeRepo(did: string): Promise { // Try local first try { - const res = await fetch(`/content/${did}/describe.json`) + const res = await fetch(`/at/${did}/describe.json`) if (res.ok && isJsonResponse(res)) { const data = await res.json() return data.collections || [] @@ -392,12 +392,12 @@ export async function getChatMessages( async function loadForDid(did: string): Promise { // Try local first try { - const res = await fetch(`/content/${did}/${collection}/index.json`) + const res = await fetch(`/at/${did}/${collection}/index.json`) if (res.ok && isJsonResponse(res)) { const rkeys: string[] = await res.json() // Load all messages in parallel const msgPromises = rkeys.map(async (rkey) => { - const msgRes = await fetch(`/content/${did}/${collection}/${rkey}.json`) + const msgRes = await fetch(`/at/${did}/${collection}/${rkey}.json`) if (msgRes.ok && isJsonResponse(msgRes)) { return msgRes.json() as Promise } @@ -590,7 +590,7 @@ export async function getCards( ): Promise { // Try local first try { - const res = await fetch(`/content/${did}/${collection}/self.json`) + const res = await fetch(`/at/${did}/${collection}/self.json`) if (res.ok && isJsonResponse(res)) { const record = await res.json() return record.value as CardCollection @@ -639,7 +639,7 @@ export async function getRse(did: string): Promise { // Try local first try { - const res = await fetch(`/content/${did}/${collection}/self.json`) + const res = await fetch(`/at/${did}/${collection}/self.json`) if (res.ok && isJsonResponse(res)) { const record = await res.json() return record.value as RseCollection @@ -701,7 +701,7 @@ export async function getCardAdmin(did: string): Promise { // Try local first try { - const res = await fetch(`/content/${did}/${collection}/self.json`) + const res = await fetch(`/at/${did}/${collection}/self.json`) if (res.ok && isJsonResponse(res)) { const record = await res.json() return record.value as CardAdminData @@ -751,7 +751,7 @@ export async function getRseAdmin(did: string): Promise { // Try local first try { - const res = await fetch(`/content/${did}/${collection}/self.json`) + const res = await fetch(`/at/${did}/${collection}/self.json`) if (res.ok && isJsonResponse(res)) { const record = await res.json() return record.value as RseAdminData @@ -784,7 +784,7 @@ export async function getLinks(did: string): Promise { // Try local first try { - const res = await fetch(`/content/${did}/${collection}/self.json`) + const res = await fetch(`/at/${did}/${collection}/self.json`) if (res.ok && isJsonResponse(res)) { const record = await res.json() return record.value as LinkCollection