test cli stream
This commit is contained in:
		@@ -49,6 +49,7 @@ regex = "1.0"
 | 
				
			|||||||
tokio-tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots", "connect"], default-features = false }
 | 
					tokio-tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots", "connect"], default-features = false }
 | 
				
			||||||
futures-util = "0.3"
 | 
					futures-util = "0.3"
 | 
				
			||||||
tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots"], default-features = false }
 | 
					tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots"], default-features = false }
 | 
				
			||||||
 | 
					rpassword = "7.3"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dev-dependencies]
 | 
					[dev-dependencies]
 | 
				
			||||||
tempfile = "3.14"
 | 
					tempfile = "3.14"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,7 +19,7 @@ provider = "ollama"
 | 
				
			|||||||
model = "gemma3:4b"
 | 
					model = "gemma3:4b"
 | 
				
			||||||
host = "https://ollama.syui.ai"
 | 
					host = "https://ollama.syui.ai"
 | 
				
			||||||
system_prompt = "あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。"
 | 
					system_prompt = "あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。"
 | 
				
			||||||
ai_handle = "ai.syui.ai"
 | 
					handle = "ai.syui.ai"
 | 
				
			||||||
#num_predict = 200
 | 
					#num_predict = 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[oauth]
 | 
					[oauth]
 | 
				
			||||||
@@ -27,5 +27,5 @@ json = "client-metadata.json"
 | 
				
			|||||||
redirect = "oauth/callback"
 | 
					redirect = "oauth/callback"
 | 
				
			||||||
admin = "ai.syui.ai"
 | 
					admin = "ai.syui.ai"
 | 
				
			||||||
collection = "ai.syui.log"
 | 
					collection = "ai.syui.log"
 | 
				
			||||||
pds = "syu.is"  # Network configuration: "bsky.social" for Bluesky, "syu.is" for independent network
 | 
					pds = "syu.is"
 | 
				
			||||||
handle_list = ["syui.syui.ai", "yui.syui.ai", "ai.syui.ai", "syui.syu.is", "ai.syu.is", "ai.ai"]
 | 
					handle_list = ["syui.syui.ai", "yui.syui.ai", "ai.syui.ai", "syui.syu.is", "ai.syu.is", "ai.ai"]
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										20
									
								
								my-blog/oauth/.env.production
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								my-blog/oauth/.env.production
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					# Production environment variables
 | 
				
			||||||
 | 
					VITE_APP_HOST=https://syui.ai
 | 
				
			||||||
 | 
					VITE_OAUTH_CLIENT_ID=https://syui.ai/client-metadata.json
 | 
				
			||||||
 | 
					VITE_OAUTH_REDIRECT_URI=https://syui.ai/oauth/callback
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Handle-based Configuration (DIDs resolved at runtime)
 | 
				
			||||||
 | 
					VITE_ATPROTO_PDS=syu.is
 | 
				
			||||||
 | 
					VITE_ADMIN_HANDLE=ai.syui.ai
 | 
				
			||||||
 | 
					VITE_AI_HANDLE=ai.syui.ai
 | 
				
			||||||
 | 
					VITE_OAUTH_COLLECTION=ai.syui.log
 | 
				
			||||||
 | 
					VITE_ATPROTO_WEB_URL=https://bsky.app
 | 
				
			||||||
 | 
					VITE_ATPROTO_HANDLE_LIST=["syui.syui.ai", "yui.syui.ai", "ai.syui.ai", "syui.syu.is", "ai.syu.is", "ai.ai"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# AI Configuration
 | 
				
			||||||
 | 
					VITE_AI_ENABLED=true
 | 
				
			||||||
 | 
					VITE_AI_ASK_AI=true
 | 
				
			||||||
 | 
					VITE_AI_PROVIDER=ollama
 | 
				
			||||||
 | 
					VITE_AI_MODEL=gemma3:4b
 | 
				
			||||||
 | 
					VITE_AI_HOST=https://ollama.syui.ai
 | 
				
			||||||
 | 
					VITE_AI_SYSTEM_PROMPT="あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。"
 | 
				
			||||||
@@ -31,7 +31,33 @@ function App() {
 | 
				
			|||||||
  const [aiCommentRecords, setAiCommentRecords] = useState<any[]>([]);
 | 
					  const [aiCommentRecords, setAiCommentRecords] = useState<any[]>([]);
 | 
				
			||||||
  const [aiProfile, setAiProfile] = useState<any>(null);
 | 
					  const [aiProfile, setAiProfile] = useState<any>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [adminDid, setAdminDid] = useState<string | null>(null);
 | 
				
			||||||
 | 
					  const [aiDid, setAiDid] = useState<string | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // ハンドルからDIDを解決する関数
 | 
				
			||||||
 | 
					  const resolveHandleToDid = async (handle: string): Promise<string | null> => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const profile = await import('./utils/pds-detection').then(m => m.getProfileForUser(handle));
 | 
				
			||||||
 | 
					      return profile?.did || null;
 | 
				
			||||||
 | 
					    } catch {
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    // 管理者とAIのDIDを解決
 | 
				
			||||||
 | 
					    const resolveAdminAndAiDids = async () => {
 | 
				
			||||||
 | 
					      const [resolvedAdminDid, resolvedAiDid] = await Promise.all([
 | 
				
			||||||
 | 
					        resolveHandleToDid(appConfig.adminHandle),
 | 
				
			||||||
 | 
					        resolveHandleToDid(appConfig.aiHandle)
 | 
				
			||||||
 | 
					      ]);
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      setAdminDid(resolvedAdminDid || appConfig.adminDid);
 | 
				
			||||||
 | 
					      setAiDid(resolvedAiDid || appConfig.aiDid);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    resolveAdminAndAiDids();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    // Setup Jetstream WebSocket for real-time comments (optional)
 | 
					    // Setup Jetstream WebSocket for real-time comments (optional)
 | 
				
			||||||
    const setupJetstream = () => {
 | 
					    const setupJetstream = () => {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
@@ -83,13 +109,28 @@ function App() {
 | 
				
			|||||||
      return false;
 | 
					      return false;
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // キャッシュがなければ、ATProtoから取得(認証状態に関係なく)
 | 
					    // DID解決が完了してからコメントとチャット履歴を読み込む
 | 
				
			||||||
    if (!loadCachedComments()) {
 | 
					    const loadDataAfterDidResolution = () => {
 | 
				
			||||||
      loadAllComments(); // URLフィルタリングを無効にして全コメント表示
 | 
					      // キャッシュがなければ、ATProtoから取得(認証状態に関係なく)
 | 
				
			||||||
    }
 | 
					      if (!loadCachedComments()) {
 | 
				
			||||||
 | 
					        loadAllComments(); // URLフィルタリングを無効にして全コメント表示
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
    // Load AI chat history (認証状態に関係なく、全ユーザーのチャット履歴を表示)
 | 
					      // Load AI chat history (認証状態に関係なく、全ユーザーのチャット履歴を表示)
 | 
				
			||||||
    loadAiChatHistory();
 | 
					      loadAiChatHistory();
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Wait for DID resolution before loading data
 | 
				
			||||||
 | 
					    if (adminDid && aiDid) {
 | 
				
			||||||
 | 
					      loadDataAfterDidResolution();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // Wait a bit and try again
 | 
				
			||||||
 | 
					      setTimeout(() => {
 | 
				
			||||||
 | 
					        if (adminDid && aiDid) {
 | 
				
			||||||
 | 
					          loadDataAfterDidResolution();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }, 1000);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // Load AI profile from handle
 | 
					    // Load AI profile from handle
 | 
				
			||||||
    const loadAiProfile = async () => {
 | 
					    const loadAiProfile = async () => {
 | 
				
			||||||
@@ -198,7 +239,7 @@ function App() {
 | 
				
			|||||||
        loadAiChatHistory();
 | 
					        loadAiChatHistory();
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        // Load user list records if admin
 | 
					        // Load user list records if admin
 | 
				
			||||||
        if (userProfile.did === appConfig.adminDid) {
 | 
					        if (userProfile.did === adminDid) {
 | 
				
			||||||
          loadUserListRecords();
 | 
					          loadUserListRecords();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
@@ -224,7 +265,7 @@ function App() {
 | 
				
			|||||||
        loadAllComments();
 | 
					        loadAllComments();
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        // Load user list records if admin
 | 
					        // Load user list records if admin
 | 
				
			||||||
        if (verifiedUser.did === appConfig.adminDid) {
 | 
					        if (verifiedUser.did === adminDid) {
 | 
				
			||||||
          loadUserListRecords();
 | 
					          loadUserListRecords();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -244,6 +285,15 @@ function App() {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
  }, []);
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // DID解決完了時にデータを再読み込み
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (adminDid && aiDid) {
 | 
				
			||||||
 | 
					      console.log('DIDs resolved, loading comments and chat history...');
 | 
				
			||||||
 | 
					      loadAllComments();
 | 
				
			||||||
 | 
					      loadAiChatHistory();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [adminDid, aiDid]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const getUserProfile = async (did: string, handle: string): Promise<User> => {
 | 
					  const getUserProfile = async (did: string, handle: string): Promise<User> => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const agent = atprotoOAuthService.getAgent();
 | 
					      const agent = atprotoOAuthService.getAgent();
 | 
				
			||||||
@@ -281,21 +331,21 @@ function App() {
 | 
				
			|||||||
  const loadAiChatHistory = async () => {
 | 
					  const loadAiChatHistory = async () => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      // Load all chat records from users in admin's user list
 | 
					      // Load all chat records from users in admin's user list
 | 
				
			||||||
      const adminDid = appConfig.adminDid;
 | 
					      const currentAdminDid = adminDid || appConfig.adminDid;
 | 
				
			||||||
      const atprotoApi = appConfig.atprotoApi || 'https://bsky.social';
 | 
					      const atprotoApi = appConfig.atprotoApi || 'https://bsky.social';
 | 
				
			||||||
      const collections = getCollectionNames(appConfig.collections.base);
 | 
					      const collections = getCollectionNames(appConfig.collections.base);
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      // First, get user list from admin using their proper PDS
 | 
					      // First, get user list from admin using their proper PDS
 | 
				
			||||||
      let adminPdsEndpoint;
 | 
					      let adminPdsEndpoint;
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const resolved = await import('./utils/pds-detection').then(m => m.resolvePdsFromRepo(adminDid));
 | 
					        const resolved = await import('./utils/pds-detection').then(m => m.resolvePdsFromRepo(currentAdminDid));
 | 
				
			||||||
        const config = await import('./utils/pds-detection').then(m => m.getNetworkConfigFromPdsEndpoint(resolved.pds));
 | 
					        const config = await import('./utils/pds-detection').then(m => m.getNetworkConfigFromPdsEndpoint(resolved.pds));
 | 
				
			||||||
        adminPdsEndpoint = config.pdsApi;
 | 
					        adminPdsEndpoint = config.pdsApi;
 | 
				
			||||||
      } catch {
 | 
					      } catch {
 | 
				
			||||||
        adminPdsEndpoint = atprotoApi;
 | 
					        adminPdsEndpoint = atprotoApi;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      const userListResponse = await fetch(`${adminPdsEndpoint}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(collections.user)}&limit=100`);
 | 
					      const userListResponse = await fetch(`${adminPdsEndpoint}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(currentAdminDid)}&collection=${encodeURIComponent(collections.user)}&limit=100`);
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      if (!userListResponse.ok) {
 | 
					      if (!userListResponse.ok) {
 | 
				
			||||||
        setAiChatHistory([]);
 | 
					        setAiChatHistory([]);
 | 
				
			||||||
@@ -318,7 +368,7 @@ function App() {
 | 
				
			|||||||
      });
 | 
					      });
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      // Always include admin DID to check admin's own chats
 | 
					      // Always include admin DID to check admin's own chats
 | 
				
			||||||
      allUserDids.push(adminDid);
 | 
					      allUserDids.push(currentAdminDid);
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      const userDids = [...new Set(allUserDids)];
 | 
					      const userDids = [...new Set(allUserDids)];
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
@@ -386,7 +436,7 @@ function App() {
 | 
				
			|||||||
  // Load AI generated content from admin DID
 | 
					  // Load AI generated content from admin DID
 | 
				
			||||||
  const loadAIGeneratedContent = async () => {
 | 
					  const loadAIGeneratedContent = async () => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const adminDid = appConfig.adminDid;
 | 
					      const currentAdminDid = adminDid || appConfig.adminDid;
 | 
				
			||||||
      const atprotoApi = appConfig.atprotoApi || 'https://bsky.social';
 | 
					      const atprotoApi = appConfig.atprotoApi || 'https://bsky.social';
 | 
				
			||||||
      const collections = getCollectionNames(appConfig.collections.base);
 | 
					      const collections = getCollectionNames(appConfig.collections.base);
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
@@ -505,32 +555,40 @@ function App() {
 | 
				
			|||||||
  const loadUsersFromRecord = async () => {
 | 
					  const loadUsersFromRecord = async () => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      // 管理者のユーザーリストを取得 using proper PDS detection
 | 
					      // 管理者のユーザーリストを取得 using proper PDS detection
 | 
				
			||||||
      const adminDid = appConfig.adminDid;
 | 
					      const currentAdminDid = adminDid || appConfig.adminDid;
 | 
				
			||||||
 | 
					      console.log('loadUsersFromRecord: Using Admin DID:', currentAdminDid);
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      // Use per-user PDS detection for admin's records
 | 
					      // Use per-user PDS detection for admin's records
 | 
				
			||||||
      let adminPdsEndpoint;
 | 
					      let adminPdsEndpoint;
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const resolved = await import('./utils/pds-detection').then(m => m.resolvePdsFromRepo(adminDid));
 | 
					        const resolved = await import('./utils/pds-detection').then(m => m.resolvePdsFromRepo(currentAdminDid));
 | 
				
			||||||
        const config = await import('./utils/pds-detection').then(m => m.getNetworkConfigFromPdsEndpoint(resolved.pds));
 | 
					        const config = await import('./utils/pds-detection').then(m => m.getNetworkConfigFromPdsEndpoint(resolved.pds));
 | 
				
			||||||
        adminPdsEndpoint = config.pdsApi;
 | 
					        adminPdsEndpoint = config.pdsApi;
 | 
				
			||||||
      } catch {
 | 
					      } catch {
 | 
				
			||||||
        adminPdsEndpoint = 'https://bsky.social'; // Fallback
 | 
					        adminPdsEndpoint = 'https://bsky.social'; // Fallback
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      const response = await fetch(`${adminPdsEndpoint}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(getCollectionNames(appConfig.collections.base).user)}&limit=100`);
 | 
					      const userCollectionUrl = `${adminPdsEndpoint}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(currentAdminDid)}&collection=${encodeURIComponent(getCollectionNames(appConfig.collections.base).user)}&limit=100`;
 | 
				
			||||||
 | 
					      console.log('loadUsersFromRecord: Fetching from URL:', userCollectionUrl);
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      const response = await fetch(userCollectionUrl);
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      console.log('loadUsersFromRecord: Response status:', response.status);
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      if (!response.ok) {
 | 
					      if (!response.ok) {
 | 
				
			||||||
        // Failed to fetch user list from admin, using default users
 | 
					        console.log('loadUsersFromRecord: Failed to fetch, using default users');
 | 
				
			||||||
        return getDefaultUsers();
 | 
					        return getDefaultUsers();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      const data = await response.json();
 | 
					      const data = await response.json();
 | 
				
			||||||
      const userRecords = data.records || [];
 | 
					      const userRecords = data.records || [];
 | 
				
			||||||
      // User records found
 | 
					      console.log('loadUsersFromRecord: Found user records:', userRecords.length);
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      if (userRecords.length === 0) {
 | 
					      if (userRecords.length === 0) {
 | 
				
			||||||
        // No user records found, using default users
 | 
					        console.log('loadUsersFromRecord: No user records found, using default users');
 | 
				
			||||||
        return getDefaultUsers();
 | 
					        const defaultUsers = getDefaultUsers();
 | 
				
			||||||
 | 
					        console.log('loadUsersFromRecord: Default users:', defaultUsers);
 | 
				
			||||||
 | 
					        return defaultUsers;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      // レコードからユーザーリストを構築し、プレースホルダーDIDを実際のDIDに解決
 | 
					      // レコードからユーザーリストを構築し、プレースホルダーDIDを実際のDIDに解決
 | 
				
			||||||
@@ -562,7 +620,7 @@ function App() {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      // Loaded and resolved users from admin records
 | 
					      console.log('loadUsersFromRecord: Resolved users:', allUsers);
 | 
				
			||||||
      return allUsers;
 | 
					      return allUsers;
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch (err) {
 | 
				
			||||||
      // Failed to load users from records, using defaults
 | 
					      // Failed to load users from records, using defaults
 | 
				
			||||||
@@ -574,19 +632,19 @@ function App() {
 | 
				
			|||||||
  const loadUserListRecords = async () => {
 | 
					  const loadUserListRecords = async () => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      // Loading user list records using proper PDS detection
 | 
					      // Loading user list records using proper PDS detection
 | 
				
			||||||
      const adminDid = appConfig.adminDid;
 | 
					      const currentAdminDid = adminDid || appConfig.adminDid;
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      // Use per-user PDS detection for admin's records
 | 
					      // Use per-user PDS detection for admin's records
 | 
				
			||||||
      let adminPdsEndpoint;
 | 
					      let adminPdsEndpoint;
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const resolved = await import('./utils/pds-detection').then(m => m.resolvePdsFromRepo(adminDid));
 | 
					        const resolved = await import('./utils/pds-detection').then(m => m.resolvePdsFromRepo(currentAdminDid));
 | 
				
			||||||
        const config = await import('./utils/pds-detection').then(m => m.getNetworkConfigFromPdsEndpoint(resolved.pds));
 | 
					        const config = await import('./utils/pds-detection').then(m => m.getNetworkConfigFromPdsEndpoint(resolved.pds));
 | 
				
			||||||
        adminPdsEndpoint = config.pdsApi;
 | 
					        adminPdsEndpoint = config.pdsApi;
 | 
				
			||||||
      } catch {
 | 
					      } catch {
 | 
				
			||||||
        adminPdsEndpoint = 'https://bsky.social'; // Fallback
 | 
					        adminPdsEndpoint = 'https://bsky.social'; // Fallback
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      const response = await fetch(`${adminPdsEndpoint}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(getCollectionNames(appConfig.collections.base).user)}&limit=100`);
 | 
					      const response = await fetch(`${adminPdsEndpoint}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(currentAdminDid)}&collection=${encodeURIComponent(getCollectionNames(appConfig.collections.base).user)}&limit=100`);
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      if (!response.ok) {
 | 
					      if (!response.ok) {
 | 
				
			||||||
        // Failed to fetch user list records
 | 
					        // Failed to fetch user list records
 | 
				
			||||||
@@ -611,21 +669,27 @@ function App() {
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const getDefaultUsers = () => {
 | 
					  const getDefaultUsers = () => {
 | 
				
			||||||
 | 
					    const currentAdminDid = adminDid || appConfig.adminDid;
 | 
				
			||||||
    const defaultUsers = [
 | 
					    const defaultUsers = [
 | 
				
			||||||
      // Default admin user
 | 
					      // Default admin user
 | 
				
			||||||
      { did: appConfig.adminDid, handle: 'syui.ai', pds: 'https://bsky.social' },
 | 
					      { did: currentAdminDid, handle: appConfig.adminHandle, pds: 'https://syu.is' },
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // 現在ログインしているユーザーも追加(重複チェック)
 | 
					    // 現在ログインしているユーザーも追加(重複チェック)
 | 
				
			||||||
    if (user && user.did && user.handle && !defaultUsers.find(u => u.did === user.did)) {
 | 
					    if (user && user.did && user.handle && !defaultUsers.find(u => u.did === user.did)) {
 | 
				
			||||||
 | 
					      // Detect PDS based on handle
 | 
				
			||||||
 | 
					      const userPds = user.handle.endsWith('.syu.is') ? 'https://syu.is' : 
 | 
				
			||||||
 | 
					                     user.handle.endsWith('.syui.ai') ? 'https://syu.is' :
 | 
				
			||||||
 | 
					                     'https://bsky.social';
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
      defaultUsers.push({
 | 
					      defaultUsers.push({
 | 
				
			||||||
        did: user.did,
 | 
					        did: user.did,
 | 
				
			||||||
        handle: user.handle,
 | 
					        handle: user.handle,
 | 
				
			||||||
        pds: user.handle.endsWith('.syu.is') ? 'https://syu.is' : 'https://bsky.social'
 | 
					        pds: userPds
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // Default users list (including current user)
 | 
					    console.log('getDefaultUsers: Returning default users:', defaultUsers);
 | 
				
			||||||
    return defaultUsers;
 | 
					    return defaultUsers;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -635,6 +699,7 @@ function App() {
 | 
				
			|||||||
      
 | 
					      
 | 
				
			||||||
      // ユーザーリストを動的に取得
 | 
					      // ユーザーリストを動的に取得
 | 
				
			||||||
      const knownUsers = await loadUsersFromRecord();
 | 
					      const knownUsers = await loadUsersFromRecord();
 | 
				
			||||||
 | 
					      console.log('loadAllComments: Using users for comment fetching:', knownUsers);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const allComments = [];
 | 
					      const allComments = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -888,7 +953,7 @@ function App() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // 管理者チェック
 | 
					  // 管理者チェック
 | 
				
			||||||
  const isAdmin = (user: User | null): boolean => {
 | 
					  const isAdmin = (user: User | null): boolean => {
 | 
				
			||||||
    return user?.did === appConfig.adminDid;
 | 
					    return user?.did === adminDid || user?.did === appConfig.adminDid;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // ユーザーリスト投稿
 | 
					  // ユーザーリスト投稿
 | 
				
			||||||
@@ -1182,6 +1247,25 @@ function App() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className="app">
 | 
					    <div className="app">
 | 
				
			||||||
 | 
					      {/* Debug Info */}
 | 
				
			||||||
 | 
					      <div style={{ 
 | 
				
			||||||
 | 
					        padding: '10px', 
 | 
				
			||||||
 | 
					        backgroundColor: '#f0f0f0', 
 | 
				
			||||||
 | 
					        border: '1px solid #ccc', 
 | 
				
			||||||
 | 
					        marginBottom: '10px',
 | 
				
			||||||
 | 
					        fontSize: '12px'
 | 
				
			||||||
 | 
					      }}>
 | 
				
			||||||
 | 
					        <strong>Debug Info:</strong><br />
 | 
				
			||||||
 | 
					        Admin Handle: {appConfig.adminHandle}<br />
 | 
				
			||||||
 | 
					        Admin DID (resolved): {adminDid || 'resolving...'}<br />
 | 
				
			||||||
 | 
					        Admin DID (config): {appConfig.adminDid}<br />
 | 
				
			||||||
 | 
					        AI Handle: {appConfig.aiHandle}<br />
 | 
				
			||||||
 | 
					        AI DID (resolved): {aiDid || 'resolving...'}<br />
 | 
				
			||||||
 | 
					        AI DID (config): {appConfig.aiDid}<br />
 | 
				
			||||||
 | 
					        Collection Base: {appConfig.collections.base}<br />
 | 
				
			||||||
 | 
					        User Collection: {appConfig.collections.base}.user<br />
 | 
				
			||||||
 | 
					        DIDs Resolved: {adminDid && aiDid ? 'Yes' : 'No'}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <main className="app-main">
 | 
					      <main className="app-main">
 | 
				
			||||||
        <section className="comment-section">
 | 
					        <section className="comment-section">
 | 
				
			||||||
@@ -1442,7 +1526,7 @@ function App() {
 | 
				
			|||||||
                aiChatHistory.map((record, index) => {
 | 
					                aiChatHistory.map((record, index) => {
 | 
				
			||||||
                  // For AI responses, use AI DID; for user questions, use the actual author
 | 
					                  // For AI responses, use AI DID; for user questions, use the actual author
 | 
				
			||||||
                  const isAiResponse = record.value.type === 'answer';
 | 
					                  const isAiResponse = record.value.type === 'answer';
 | 
				
			||||||
                  const displayDid = isAiResponse ? appConfig.aiDid : record.value.author?.did;
 | 
					                  const displayDid = isAiResponse ? (aiDid || appConfig.aiDid) : record.value.author?.did;
 | 
				
			||||||
                  const displayHandle = isAiResponse ? (aiProfile?.handle || 'yui.syui.ai') : record.value.author?.handle;
 | 
					                  const displayHandle = isAiResponse ? (aiProfile?.handle || 'yui.syui.ai') : record.value.author?.handle;
 | 
				
			||||||
                  const displayName = isAiResponse ? 'AI' : (record.value.author?.displayName || record.value.author?.handle);
 | 
					                  const displayName = isAiResponse ? 'AI' : (record.value.author?.displayName || record.value.author?.handle);
 | 
				
			||||||
                  
 | 
					                  
 | 
				
			||||||
@@ -1558,8 +1642,9 @@ function App() {
 | 
				
			|||||||
                        className="comment-avatar"
 | 
					                        className="comment-avatar"
 | 
				
			||||||
                        ref={(img) => {
 | 
					                        ref={(img) => {
 | 
				
			||||||
                          // Fetch AI avatar
 | 
					                          // Fetch AI avatar
 | 
				
			||||||
                          if (img && appConfig.aiDid) {
 | 
					                          const currentAiDid = aiDid || appConfig.aiDid;
 | 
				
			||||||
                            fetch(`${appConfig.bskyPublicApi}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(appConfig.aiDid)}`)
 | 
					                          if (img && currentAiDid) {
 | 
				
			||||||
 | 
					                            fetch(`${appConfig.bskyPublicApi}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(currentAiDid)}`)
 | 
				
			||||||
                              .then(res => res.json())
 | 
					                              .then(res => res.json())
 | 
				
			||||||
                              .then(data => {
 | 
					                              .then(data => {
 | 
				
			||||||
                                if (data.avatar && img) {
 | 
					                                if (data.avatar && img) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -84,10 +84,12 @@ function extractRkeyFromUrl(): string | undefined {
 | 
				
			|||||||
// Get application configuration from environment variables
 | 
					// Get application configuration from environment variables
 | 
				
			||||||
export function getAppConfig(): AppConfig {
 | 
					export function getAppConfig(): AppConfig {
 | 
				
			||||||
  const host = import.meta.env.VITE_APP_HOST || 'https://log.syui.ai';
 | 
					  const host = import.meta.env.VITE_APP_HOST || 'https://log.syui.ai';
 | 
				
			||||||
 | 
					  const adminHandle = import.meta.env.VITE_ADMIN_HANDLE || 'ai.syui.ai';
 | 
				
			||||||
 | 
					  const aiHandle = import.meta.env.VITE_AI_HANDLE || 'ai.syui.ai';
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // DIDsはハンドルから実行時に解決される(フォールバック用のみ保持)
 | 
				
			||||||
  const adminDid = import.meta.env.VITE_ADMIN_DID || 'did:plc:uqzpqmrjnptsxezjx4xuh2mn';
 | 
					  const adminDid = import.meta.env.VITE_ADMIN_DID || 'did:plc:uqzpqmrjnptsxezjx4xuh2mn';
 | 
				
			||||||
  const adminHandle = import.meta.env.VITE_ADMIN_HANDLE || 'syui.ai';
 | 
					 | 
				
			||||||
  const aiDid = import.meta.env.VITE_AI_DID || 'did:plc:4hqjfn7m6n5hno3doamuhgef';
 | 
					  const aiDid = import.meta.env.VITE_AI_DID || 'did:plc:4hqjfn7m6n5hno3doamuhgef';
 | 
				
			||||||
  const aiHandle = import.meta.env.VITE_AI_HANDLE || 'yui.syui.ai';
 | 
					 | 
				
			||||||
  const aiDisplayName = import.meta.env.VITE_AI_DISPLAY_NAME || 'ai';
 | 
					  const aiDisplayName = import.meta.env.VITE_AI_DISPLAY_NAME || 'ai';
 | 
				
			||||||
  const aiAvatar = import.meta.env.VITE_AI_AVATAR || '';
 | 
					  const aiAvatar = import.meta.env.VITE_AI_AVATAR || '';
 | 
				
			||||||
  const aiDescription = import.meta.env.VITE_AI_DESCRIPTION || '';
 | 
					  const aiDescription = import.meta.env.VITE_AI_DESCRIPTION || '';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,16 +3,16 @@
 | 
				
			|||||||
set -e
 | 
					set -e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
cb=ai.syui.log
 | 
					cb=ai.syui.log
 | 
				
			||||||
cl=( $cb.chat $cb.chat.comment $cb.chat.lang )
 | 
					cl=( $cb.user )
 | 
				
			||||||
f=~/.config/syui/ai/bot/token.json
 | 
					f=~/.config/syui/ai/log/config.json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
default_collection="ai.syui.log.chat.comment"
 | 
					default_collection="ai.syui.log.chat.comment"
 | 
				
			||||||
default_pds="bsky.social"
 | 
					default_pds="syu.is"
 | 
				
			||||||
default_did=`cat $f|jq -r .did`
 | 
					default_did=`cat $f|jq -r .admin.did`
 | 
				
			||||||
default_token=`cat $f|jq -r .accessJwt`
 | 
					default_token=`cat $f|jq -r .admin.access_jwt`
 | 
				
			||||||
default_refresh=`cat $f|jq -r .refreshJwt`
 | 
					default_refresh=`cat $f|jq -r .admin.refresh_jwt`
 | 
				
			||||||
curl -sL -X POST -H "Content-Type: application/json" -H "Authorization: Bearer $default_refresh" https://$default_pds/xrpc/com.atproto.server.refreshSession >! $f
 | 
					#curl -sL -X POST -H "Content-Type: application/json" -H "Authorization: Bearer $default_refresh" https://$default_pds/xrpc/com.atproto.server.refreshSession >! $f
 | 
				
			||||||
default_token=`cat $f|jq -r .accessJwt`
 | 
					#default_token=`cat $f|jq -r .admin.access_jwt`
 | 
				
			||||||
collection=${1:-$default_collection}
 | 
					collection=${1:-$default_collection}
 | 
				
			||||||
pds=${2:-$default_pds}
 | 
					pds=${2:-$default_pds}
 | 
				
			||||||
did=${3:-$default_did}
 | 
					did=${3:-$default_did}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -87,6 +87,122 @@ fn get_config_path() -> Result<PathBuf> {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn init() -> Result<()> {
 | 
					pub async fn init() -> Result<()> {
 | 
				
			||||||
 | 
					    init_with_pds(None).await
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn init_with_options(
 | 
				
			||||||
 | 
					    pds_override: Option<String>,
 | 
				
			||||||
 | 
					    handle_override: Option<String>, 
 | 
				
			||||||
 | 
					    use_password: bool,
 | 
				
			||||||
 | 
					    access_jwt_override: Option<String>,
 | 
				
			||||||
 | 
					    refresh_jwt_override: Option<String>
 | 
				
			||||||
 | 
					) -> Result<()> {
 | 
				
			||||||
 | 
					    println!("{}", "🔐 Initializing ATProto authentication...".cyan());
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let config_path = get_config_path()?;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if config_path.exists() {
 | 
				
			||||||
 | 
					        println!("{}", "⚠️  Configuration already exists. Use 'ailog auth logout' to reset.".yellow());
 | 
				
			||||||
 | 
					        return Ok(());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Validate options
 | 
				
			||||||
 | 
					    if let (Some(_), Some(_)) = (&access_jwt_override, &refresh_jwt_override) {
 | 
				
			||||||
 | 
					        if use_password {
 | 
				
			||||||
 | 
					            println!("{}", "⚠️  Cannot use both --password and JWT tokens. Choose one method.".yellow());
 | 
				
			||||||
 | 
					            return Ok(());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else if access_jwt_override.is_some() || refresh_jwt_override.is_some() {
 | 
				
			||||||
 | 
					        println!("{}", "❌ Both --access-jwt and --refresh-jwt must be provided together.".red());
 | 
				
			||||||
 | 
					        return Ok(());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    println!("{}", "📋 Please provide your ATProto credentials:".cyan());
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Get handle
 | 
				
			||||||
 | 
					    let handle = if let Some(h) = handle_override {
 | 
				
			||||||
 | 
					        h
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        print!("Handle (e.g., your.handle.bsky.social): ");
 | 
				
			||||||
 | 
					        std::io::Write::flush(&mut std::io::stdout())?;
 | 
				
			||||||
 | 
					        let mut input = String::new();
 | 
				
			||||||
 | 
					        std::io::stdin().read_line(&mut input)?;
 | 
				
			||||||
 | 
					        input.trim().to_string()
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Determine PDS URL
 | 
				
			||||||
 | 
					    let pds_url = if let Some(override_pds) = pds_override {
 | 
				
			||||||
 | 
					        if override_pds.starts_with("http") {
 | 
				
			||||||
 | 
					            override_pds
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            format!("https://{}", override_pds)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        if handle.ends_with(".syu.is") {
 | 
				
			||||||
 | 
					            "https://syu.is".to_string()
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            "https://bsky.social".to_string()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    println!("{}", format!("🌐 Using PDS: {}", pds_url).cyan());
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Get credentials
 | 
				
			||||||
 | 
					    let (access_jwt, refresh_jwt) = if let (Some(access), Some(refresh)) = (access_jwt_override, refresh_jwt_override) {
 | 
				
			||||||
 | 
					        println!("{}", "🔑 Using provided JWT tokens".cyan());
 | 
				
			||||||
 | 
					        (access, refresh)
 | 
				
			||||||
 | 
					    } else if use_password {
 | 
				
			||||||
 | 
					        println!("{}", "🔒 Using password authentication".cyan());
 | 
				
			||||||
 | 
					        authenticate_with_password(&handle, &pds_url).await?
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        // Interactive JWT input (legacy behavior)
 | 
				
			||||||
 | 
					        print!("Access JWT: ");
 | 
				
			||||||
 | 
					        std::io::Write::flush(&mut std::io::stdout())?;
 | 
				
			||||||
 | 
					        let mut access_jwt = String::new();
 | 
				
			||||||
 | 
					        std::io::stdin().read_line(&mut access_jwt)?;
 | 
				
			||||||
 | 
					        let access_jwt = access_jwt.trim().to_string();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        print!("Refresh JWT: ");
 | 
				
			||||||
 | 
					        std::io::Write::flush(&mut std::io::stdout())?;
 | 
				
			||||||
 | 
					        let mut refresh_jwt = String::new();
 | 
				
			||||||
 | 
					        std::io::stdin().read_line(&mut refresh_jwt)?;
 | 
				
			||||||
 | 
					        let refresh_jwt = refresh_jwt.trim().to_string();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        (access_jwt, refresh_jwt)
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Resolve DID from handle
 | 
				
			||||||
 | 
					    println!("{}", "🔍 Resolving DID from handle...".cyan());
 | 
				
			||||||
 | 
					    let did = resolve_did_with_pds(&handle, &pds_url).await?;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Create config
 | 
				
			||||||
 | 
					    let config = AuthConfig {
 | 
				
			||||||
 | 
					        admin: AdminConfig {
 | 
				
			||||||
 | 
					            did: did.clone(),
 | 
				
			||||||
 | 
					            handle: handle.clone(),
 | 
				
			||||||
 | 
					            access_jwt,
 | 
				
			||||||
 | 
					            refresh_jwt,
 | 
				
			||||||
 | 
					            pds: pds_url,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        jetstream: JetstreamConfig {
 | 
				
			||||||
 | 
					            url: "wss://jetstream2.us-east.bsky.network/subscribe".to_string(),
 | 
				
			||||||
 | 
					            collections: vec!["ai.syui.log".to_string()],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        collections: generate_collection_config(),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Save config
 | 
				
			||||||
 | 
					    let config_json = serde_json::to_string_pretty(&config)?;
 | 
				
			||||||
 | 
					    fs::write(&config_path, config_json)?;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    println!("{}", "✅ Authentication configured successfully!".green());
 | 
				
			||||||
 | 
					    println!("📁 Config saved to: {}", config_path.display());
 | 
				
			||||||
 | 
					    println!("👤 Authenticated as: {} ({})", handle, did);
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn init_with_pds(pds_override: Option<String>) -> Result<()> {
 | 
				
			||||||
    println!("{}", "🔐 Initializing ATProto authentication...".cyan());
 | 
					    println!("{}", "🔐 Initializing ATProto authentication...".cyan());
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    let config_path = get_config_path()?;
 | 
					    let config_path = get_config_path()?;
 | 
				
			||||||
@@ -117,9 +233,28 @@ pub async fn init() -> Result<()> {
 | 
				
			|||||||
    std::io::stdin().read_line(&mut refresh_jwt)?;
 | 
					    std::io::stdin().read_line(&mut refresh_jwt)?;
 | 
				
			||||||
    let refresh_jwt = refresh_jwt.trim().to_string();
 | 
					    let refresh_jwt = refresh_jwt.trim().to_string();
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    // Determine PDS URL
 | 
				
			||||||
 | 
					    let pds_url = if let Some(override_pds) = pds_override {
 | 
				
			||||||
 | 
					        // Use provided PDS override
 | 
				
			||||||
 | 
					        if override_pds.starts_with("http") {
 | 
				
			||||||
 | 
					            override_pds
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            format!("https://{}", override_pds)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        // Auto-detect from handle suffix
 | 
				
			||||||
 | 
					        if handle.ends_with(".syu.is") {
 | 
				
			||||||
 | 
					            "https://syu.is".to_string()
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            "https://bsky.social".to_string()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    println!("{}", format!("🌐 Using PDS: {}", pds_url).cyan());
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    // Resolve DID from handle
 | 
					    // Resolve DID from handle
 | 
				
			||||||
    println!("{}", "🔍 Resolving DID from handle...".cyan());
 | 
					    println!("{}", "🔍 Resolving DID from handle...".cyan());
 | 
				
			||||||
    let did = resolve_did(&handle).await?;
 | 
					    let did = resolve_did_with_pds(&handle, &pds_url).await?;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // Create config
 | 
					    // Create config
 | 
				
			||||||
    let config = AuthConfig {
 | 
					    let config = AuthConfig {
 | 
				
			||||||
@@ -128,11 +263,7 @@ pub async fn init() -> Result<()> {
 | 
				
			|||||||
            handle: handle.clone(),
 | 
					            handle: handle.clone(),
 | 
				
			||||||
            access_jwt,
 | 
					            access_jwt,
 | 
				
			||||||
            refresh_jwt,
 | 
					            refresh_jwt,
 | 
				
			||||||
            pds: if handle.ends_with(".syu.is") {
 | 
					            pds: pds_url,
 | 
				
			||||||
                "https://syu.is".to_string()
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                "https://bsky.social".to_string()
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        jetstream: JetstreamConfig {
 | 
					        jetstream: JetstreamConfig {
 | 
				
			||||||
            url: "wss://jetstream2.us-east.bsky.network/subscribe".to_string(),
 | 
					            url: "wss://jetstream2.us-east.bsky.network/subscribe".to_string(),
 | 
				
			||||||
@@ -178,6 +309,93 @@ async fn resolve_did(handle: &str) -> Result<String> {
 | 
				
			|||||||
    Ok(did.to_string())
 | 
					    Ok(did.to_string())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn resolve_did_with_pds(handle: &str, pds_url: &str) -> Result<String> {
 | 
				
			||||||
 | 
					    let client = reqwest::Client::new();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Try to use the PDS API first
 | 
				
			||||||
 | 
					    let api_base = if pds_url.contains("syu.is") {
 | 
				
			||||||
 | 
					        "https://bsky.syu.is"
 | 
				
			||||||
 | 
					    } else if pds_url.contains("bsky.social") {
 | 
				
			||||||
 | 
					        "https://public.api.bsky.app"
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        // For custom PDS, try to construct API URL
 | 
				
			||||||
 | 
					        pds_url
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let url = format!("{}/xrpc/app.bsky.actor.getProfile?actor={}", 
 | 
				
			||||||
 | 
					                     api_base, urlencoding::encode(handle));
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let response = client.get(&url).send().await?;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if !response.status().is_success() {
 | 
				
			||||||
 | 
					        return Err(anyhow::anyhow!("Failed to resolve handle using PDS {}: {}", pds_url, response.status()));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let profile: serde_json::Value = response.json().await?;
 | 
				
			||||||
 | 
					    let did = profile["did"].as_str()
 | 
				
			||||||
 | 
					        .ok_or_else(|| anyhow::anyhow!("DID not found in profile response"))?;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    Ok(did.to_string())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn authenticate_with_password(handle: &str, pds_url: &str) -> Result<(String, String)> {
 | 
				
			||||||
 | 
					    use std::io::{self, Write};
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Get password securely
 | 
				
			||||||
 | 
					    print!("Password: ");
 | 
				
			||||||
 | 
					    io::stdout().flush()?;
 | 
				
			||||||
 | 
					    let password = rpassword::read_password()
 | 
				
			||||||
 | 
					        .context("Failed to read password")?;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if password.is_empty() {
 | 
				
			||||||
 | 
					        return Err(anyhow::anyhow!("Password cannot be empty"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    println!("{}", "🔐 Authenticating with ATProto server...".cyan());
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let client = reqwest::Client::new();
 | 
				
			||||||
 | 
					    let auth_url = format!("{}/xrpc/com.atproto.server.createSession", pds_url);
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let auth_request = serde_json::json!({
 | 
				
			||||||
 | 
					        "identifier": handle,
 | 
				
			||||||
 | 
					        "password": password
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let response = client
 | 
				
			||||||
 | 
					        .post(&auth_url)
 | 
				
			||||||
 | 
					        .header("Content-Type", "application/json")
 | 
				
			||||||
 | 
					        .json(&auth_request)
 | 
				
			||||||
 | 
					        .send()
 | 
				
			||||||
 | 
					        .await?;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if !response.status().is_success() {
 | 
				
			||||||
 | 
					        let status = response.status();
 | 
				
			||||||
 | 
					        let error_text = response.text().await.unwrap_or_default();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if status.as_u16() == 401 {
 | 
				
			||||||
 | 
					            return Err(anyhow::anyhow!("Authentication failed: Invalid handle or password"));
 | 
				
			||||||
 | 
					        } else if status.as_u16() == 400 {
 | 
				
			||||||
 | 
					            return Err(anyhow::anyhow!("Authentication failed: Bad request (check handle format)"));
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            return Err(anyhow::anyhow!("Authentication failed: {} - {}", status, error_text));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let auth_response: serde_json::Value = response.json().await?;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let access_jwt = auth_response["accessJwt"].as_str()
 | 
				
			||||||
 | 
					        .ok_or_else(|| anyhow::anyhow!("No access JWT in response"))?
 | 
				
			||||||
 | 
					        .to_string();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					    let refresh_jwt = auth_response["refreshJwt"].as_str()
 | 
				
			||||||
 | 
					        .ok_or_else(|| anyhow::anyhow!("No refresh JWT in response"))?
 | 
				
			||||||
 | 
					        .to_string();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    println!("{}", "✅ Password authentication successful".green());
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    Ok((access_jwt, refresh_jwt))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn status() -> Result<()> {
 | 
					pub async fn status() -> Result<()> {
 | 
				
			||||||
    let config_path = get_config_path()?;
 | 
					    let config_path = get_config_path()?;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
@@ -200,9 +418,17 @@ pub async fn status() -> Result<()> {
 | 
				
			|||||||
    
 | 
					    
 | 
				
			||||||
    // Test API access
 | 
					    // Test API access
 | 
				
			||||||
    println!("\n{}", "🧪 Testing API access...".cyan());
 | 
					    println!("\n{}", "🧪 Testing API access...".cyan());
 | 
				
			||||||
    match test_api_access(&config).await {
 | 
					    match test_api_access_with_auth(&config).await {
 | 
				
			||||||
        Ok(_) => println!("{}", "✅ API access successful".green()),
 | 
					        Ok(_) => println!("{}", "✅ API access successful".green()),
 | 
				
			||||||
        Err(e) => println!("{}", format!("❌ API access failed: {}", e).red()),
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            println!("{}", format!("❌ Authenticated API access failed: {}", e).red());
 | 
				
			||||||
 | 
					            // Fallback to public API test
 | 
				
			||||||
 | 
					            println!("{}", "🔄 Trying public API access...".cyan());
 | 
				
			||||||
 | 
					            match test_api_access(&config).await {
 | 
				
			||||||
 | 
					                Ok(_) => println!("{}", "✅ Public API access successful".green()),
 | 
				
			||||||
 | 
					                Err(e2) => println!("{}", format!("❌ Public API access also failed: {}", e2).red()),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
use anyhow::Result;
 | 
					use anyhow::Result;
 | 
				
			||||||
use colored::Colorize;
 | 
					use colored::Colorize;
 | 
				
			||||||
use std::path::PathBuf;
 | 
					use std::path::PathBuf;
 | 
				
			||||||
 | 
					use std::fs;
 | 
				
			||||||
use crate::generator::Generator;
 | 
					use crate::generator::Generator;
 | 
				
			||||||
use crate::config::Config;
 | 
					use crate::config::Config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -10,6 +11,12 @@ pub async fn execute(path: PathBuf) -> Result<()> {
 | 
				
			|||||||
    // Load configuration
 | 
					    // Load configuration
 | 
				
			||||||
    let config = Config::load(&path)?;
 | 
					    let config = Config::load(&path)?;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    // Generate OAuth .env.production if oauth directory exists
 | 
				
			||||||
 | 
					    let oauth_dir = path.join("oauth");
 | 
				
			||||||
 | 
					    if oauth_dir.exists() {
 | 
				
			||||||
 | 
					        generate_oauth_env(&path, &config)?;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    // Create generator
 | 
					    // Create generator
 | 
				
			||||||
    let generator = Generator::new(path, config)?;
 | 
					    let generator = Generator::new(path, config)?;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
@@ -20,3 +27,102 @@ pub async fn execute(path: PathBuf) -> Result<()> {
 | 
				
			|||||||
    
 | 
					    
 | 
				
			||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn generate_oauth_env(path: &PathBuf, config: &Config) -> Result<()> {
 | 
				
			||||||
 | 
					    let oauth_dir = path.join("oauth");
 | 
				
			||||||
 | 
					    let env_file = oauth_dir.join(".env.production");
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Extract configuration values
 | 
				
			||||||
 | 
					    let base_url = &config.site.base_url;
 | 
				
			||||||
 | 
					    let oauth_json = config.oauth.as_ref()
 | 
				
			||||||
 | 
					        .and_then(|o| o.json.as_ref())
 | 
				
			||||||
 | 
					        .map(|s| s.as_str())
 | 
				
			||||||
 | 
					        .unwrap_or("client-metadata.json");
 | 
				
			||||||
 | 
					    let oauth_redirect = config.oauth.as_ref()
 | 
				
			||||||
 | 
					        .and_then(|o| o.redirect.as_ref())
 | 
				
			||||||
 | 
					        .map(|s| s.as_str())
 | 
				
			||||||
 | 
					        .unwrap_or("oauth/callback");
 | 
				
			||||||
 | 
					    let admin_handle = config.oauth.as_ref()
 | 
				
			||||||
 | 
					        .and_then(|o| o.admin.as_ref())
 | 
				
			||||||
 | 
					        .map(|s| s.as_str())
 | 
				
			||||||
 | 
					        .unwrap_or("ai.syui.ai");
 | 
				
			||||||
 | 
					    let ai_handle = config.ai.as_ref()
 | 
				
			||||||
 | 
					        .and_then(|a| a.handle.as_ref())
 | 
				
			||||||
 | 
					        .map(|s| s.as_str())
 | 
				
			||||||
 | 
					        .unwrap_or("ai.syui.ai");
 | 
				
			||||||
 | 
					    let collection = config.oauth.as_ref()
 | 
				
			||||||
 | 
					        .and_then(|o| o.collection.as_ref())
 | 
				
			||||||
 | 
					        .map(|s| s.as_str())
 | 
				
			||||||
 | 
					        .unwrap_or("ai.syui.log");
 | 
				
			||||||
 | 
					    let pds = config.oauth.as_ref()
 | 
				
			||||||
 | 
					        .and_then(|o| o.pds.as_ref())
 | 
				
			||||||
 | 
					        .map(|s| s.as_str())
 | 
				
			||||||
 | 
					        .unwrap_or("syu.is");
 | 
				
			||||||
 | 
					    let handle_list = config.oauth.as_ref()
 | 
				
			||||||
 | 
					        .and_then(|o| o.handle_list.as_ref())
 | 
				
			||||||
 | 
					        .map(|list| format!("{:?}", list))
 | 
				
			||||||
 | 
					        .unwrap_or_else(|| "[\"syui.syui.ai\",\"yui.syui.ai\",\"ai.syui.ai\"]".to_string());
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // AI configuration
 | 
				
			||||||
 | 
					    let ai_enabled = config.ai.as_ref().map(|a| a.enabled).unwrap_or(true);
 | 
				
			||||||
 | 
					    let ai_ask_ai = config.ai.as_ref().and_then(|a| a.ask_ai).unwrap_or(true);
 | 
				
			||||||
 | 
					    let ai_provider = config.ai.as_ref()
 | 
				
			||||||
 | 
					        .and_then(|a| a.provider.as_ref())
 | 
				
			||||||
 | 
					        .map(|s| s.as_str())
 | 
				
			||||||
 | 
					        .unwrap_or("ollama");
 | 
				
			||||||
 | 
					    let ai_model = config.ai.as_ref()
 | 
				
			||||||
 | 
					        .and_then(|a| a.model.as_ref())
 | 
				
			||||||
 | 
					        .map(|s| s.as_str())
 | 
				
			||||||
 | 
					        .unwrap_or("gemma3:4b");
 | 
				
			||||||
 | 
					    let ai_host = config.ai.as_ref()
 | 
				
			||||||
 | 
					        .and_then(|a| a.host.as_ref())
 | 
				
			||||||
 | 
					        .map(|s| s.as_str())
 | 
				
			||||||
 | 
					        .unwrap_or("https://ollama.syui.ai");
 | 
				
			||||||
 | 
					    let ai_system_prompt = config.ai.as_ref()
 | 
				
			||||||
 | 
					        .and_then(|a| a.system_prompt.as_ref())
 | 
				
			||||||
 | 
					        .map(|s| s.as_str())
 | 
				
			||||||
 | 
					        .unwrap_or("あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。");
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let env_content = format!(
 | 
				
			||||||
 | 
					r#"# Production environment variables
 | 
				
			||||||
 | 
					VITE_APP_HOST={}
 | 
				
			||||||
 | 
					VITE_OAUTH_CLIENT_ID={}/{}
 | 
				
			||||||
 | 
					VITE_OAUTH_REDIRECT_URI={}/{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Handle-based Configuration (DIDs resolved at runtime)
 | 
				
			||||||
 | 
					VITE_ATPROTO_PDS={}
 | 
				
			||||||
 | 
					VITE_ADMIN_HANDLE={}
 | 
				
			||||||
 | 
					VITE_AI_HANDLE={}
 | 
				
			||||||
 | 
					VITE_OAUTH_COLLECTION={}
 | 
				
			||||||
 | 
					VITE_ATPROTO_WEB_URL=https://bsky.app
 | 
				
			||||||
 | 
					VITE_ATPROTO_HANDLE_LIST={}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# AI Configuration
 | 
				
			||||||
 | 
					VITE_AI_ENABLED={}
 | 
				
			||||||
 | 
					VITE_AI_ASK_AI={}
 | 
				
			||||||
 | 
					VITE_AI_PROVIDER={}
 | 
				
			||||||
 | 
					VITE_AI_MODEL={}
 | 
				
			||||||
 | 
					VITE_AI_HOST={}
 | 
				
			||||||
 | 
					VITE_AI_SYSTEM_PROMPT="{}"
 | 
				
			||||||
 | 
					"#,
 | 
				
			||||||
 | 
					        base_url,
 | 
				
			||||||
 | 
					        base_url, oauth_json,
 | 
				
			||||||
 | 
					        base_url, oauth_redirect,
 | 
				
			||||||
 | 
					        pds,
 | 
				
			||||||
 | 
					        admin_handle,
 | 
				
			||||||
 | 
					        ai_handle,
 | 
				
			||||||
 | 
					        collection,
 | 
				
			||||||
 | 
					        handle_list,
 | 
				
			||||||
 | 
					        ai_enabled,
 | 
				
			||||||
 | 
					        ai_ask_ai,
 | 
				
			||||||
 | 
					        ai_provider,
 | 
				
			||||||
 | 
					        ai_model,
 | 
				
			||||||
 | 
					        ai_host,
 | 
				
			||||||
 | 
					        ai_system_prompt
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    fs::write(&env_file, env_content)?;
 | 
				
			||||||
 | 
					    println!("  {} oauth/.env.production", "Generated".cyan());
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -37,9 +37,23 @@ highlight_code = true
 | 
				
			|||||||
minify = false
 | 
					minify = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[ai]
 | 
					[ai]
 | 
				
			||||||
enabled = false
 | 
					enabled = true
 | 
				
			||||||
auto_translate = false
 | 
					auto_translate = false
 | 
				
			||||||
comment_moderation = false
 | 
					comment_moderation = false
 | 
				
			||||||
 | 
					ask_ai = true
 | 
				
			||||||
 | 
					provider = "ollama"
 | 
				
			||||||
 | 
					model = "gemma3:4b"
 | 
				
			||||||
 | 
					host = "https://ollama.syui.ai"
 | 
				
			||||||
 | 
					system_prompt = "あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。"
 | 
				
			||||||
 | 
					handle = "ai.syui.ai"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[oauth]
 | 
				
			||||||
 | 
					json = "client-metadata.json"
 | 
				
			||||||
 | 
					redirect = "oauth/callback"
 | 
				
			||||||
 | 
					admin = "ai.syui.ai"
 | 
				
			||||||
 | 
					collection = "ai.syui.log"
 | 
				
			||||||
 | 
					pds = "syu.is"
 | 
				
			||||||
 | 
					handle_list = ["syui.syui.ai", "yui.syui.ai", "ai.syui.ai", "syui.syu.is", "ai.syu.is"]
 | 
				
			||||||
"#;
 | 
					"#;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fs::write(path.join("config.toml"), config_content)?;
 | 
					    fs::write(path.join("config.toml"), config_content)?;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -223,7 +223,7 @@ fn load_ai_config_from_project() -> Result<AiConfig> {
 | 
				
			|||||||
    
 | 
					    
 | 
				
			||||||
    // Read AI handle (preferred) or fallback to AI DID
 | 
					    // Read AI handle (preferred) or fallback to AI DID
 | 
				
			||||||
    let ai_handle = ai_config
 | 
					    let ai_handle = ai_config
 | 
				
			||||||
        .and_then(|ai| ai.get("ai_handle"))
 | 
					        .and_then(|ai| ai.get("handle"))
 | 
				
			||||||
        .and_then(|v| v.as_str())
 | 
					        .and_then(|v| v.as_str())
 | 
				
			||||||
        .unwrap_or("yui.syui.ai")
 | 
					        .unwrap_or("yui.syui.ai")
 | 
				
			||||||
        .to_string();
 | 
					        .to_string();
 | 
				
			||||||
@@ -340,6 +340,104 @@ fn get_pid_file() -> Result<PathBuf> {
 | 
				
			|||||||
    Ok(pid_dir.join("stream.pid"))
 | 
					    Ok(pid_dir.join("stream.pid"))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn init_user_list(project_dir: Option<PathBuf>, handles: Option<String>) -> Result<()> {
 | 
				
			||||||
 | 
					    println!("{}", "🔧 Initializing user list...".cyan());
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Load auth config
 | 
				
			||||||
 | 
					    let mut config = match load_config_with_refresh().await {
 | 
				
			||||||
 | 
					        Ok(config) => config,
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            println!("{}", format!("❌ Not authenticated: {}. Run 'ailog auth init --pds <PDS>' first.", e).red());
 | 
				
			||||||
 | 
					            return Ok(());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    println!("{}", format!("📋 Admin: {} ({})", config.admin.handle, config.admin.did).cyan());
 | 
				
			||||||
 | 
					    println!("{}", format!("🌐 PDS: {}", config.admin.pds).cyan());
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let mut users = Vec::new();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Parse handles if provided
 | 
				
			||||||
 | 
					    if let Some(handles_str) = handles {
 | 
				
			||||||
 | 
					        println!("{}", "🔍 Resolving provided handles...".cyan());
 | 
				
			||||||
 | 
					        let handle_list: Vec<&str> = handles_str.split(',').map(|s| s.trim()).collect();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        for handle in handle_list {
 | 
				
			||||||
 | 
					            if handle.is_empty() {
 | 
				
			||||||
 | 
					                continue;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            println!("   🏷️  Resolving handle: {}", handle);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Get AI config to determine network settings
 | 
				
			||||||
 | 
					            let ai_config = if let Some(ref proj_dir) = project_dir {
 | 
				
			||||||
 | 
					                let current_dir = std::env::current_dir()?;
 | 
				
			||||||
 | 
					                std::env::set_current_dir(proj_dir)?;
 | 
				
			||||||
 | 
					                let config = load_ai_config_from_project().unwrap_or_default();
 | 
				
			||||||
 | 
					                std::env::set_current_dir(current_dir)?;
 | 
				
			||||||
 | 
					                config
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                load_ai_config_from_project().unwrap_or_default()
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Try to resolve handle to DID
 | 
				
			||||||
 | 
					            match resolve_handle_to_did(handle, &ai_config.network).await {
 | 
				
			||||||
 | 
					                Ok(did) => {
 | 
				
			||||||
 | 
					                    println!("      ✅ DID: {}", did.cyan());
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Detect PDS for this user using proper detection
 | 
				
			||||||
 | 
					                    let detected_pds = detect_user_pds(&did, &ai_config.network).await
 | 
				
			||||||
 | 
					                        .unwrap_or_else(|_| {
 | 
				
			||||||
 | 
					                            // Fallback to handle-based detection
 | 
				
			||||||
 | 
					                            if handle.ends_with(".syu.is") {
 | 
				
			||||||
 | 
					                                "https://syu.is".to_string()
 | 
				
			||||||
 | 
					                            } else {
 | 
				
			||||||
 | 
					                                "https://bsky.social".to_string()
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        });
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    users.push(UserRecord {
 | 
				
			||||||
 | 
					                        did,
 | 
				
			||||||
 | 
					                        handle: handle.to_string(),
 | 
				
			||||||
 | 
					                        pds: detected_pds,
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                Err(e) => {
 | 
				
			||||||
 | 
					                    println!("      ❌ Failed to resolve {}: {}", handle, e);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        println!("{}", "ℹ️  No handles provided, creating empty user list".blue());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Create the initial user list
 | 
				
			||||||
 | 
					    println!("{}", format!("📝 Creating user list with {} users...", users.len()).cyan());
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    match post_user_list(&mut config, &users, json!({
 | 
				
			||||||
 | 
					        "reason": "initial_setup",
 | 
				
			||||||
 | 
					        "created_by": "ailog_stream_init"
 | 
				
			||||||
 | 
					    })).await {
 | 
				
			||||||
 | 
					        Ok(_) => println!("{}", "✅ User list created successfully!".green()),
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            println!("{}", format!("❌ Failed to create user list: {}", e).red());
 | 
				
			||||||
 | 
					            return Err(e);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Show summary
 | 
				
			||||||
 | 
					    if users.is_empty() {
 | 
				
			||||||
 | 
					        println!("{}", "📋 Empty user list created. Use 'ailog stream start --ai-generate' to auto-add commenters.".blue());
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        println!("{}", "📋 User list contents:".cyan());
 | 
				
			||||||
 | 
					        for user in &users {
 | 
				
			||||||
 | 
					            println!("   👤 {} ({})", user.handle, user.did);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn start(project_dir: Option<PathBuf>, daemon: bool, ai_generate: bool) -> Result<()> {
 | 
					pub async fn start(project_dir: Option<PathBuf>, daemon: bool, ai_generate: bool) -> Result<()> {
 | 
				
			||||||
    let mut config = load_config_with_refresh().await?;
 | 
					    let mut config = load_config_with_refresh().await?;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
@@ -679,6 +777,33 @@ fn get_network_config_from_pds(pds_endpoint: &str) -> NetworkConfig {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn detect_user_pds(did: &str, _network_config: &NetworkConfig) -> Result<String> {
 | 
				
			||||||
 | 
					    let client = reqwest::Client::new();
 | 
				
			||||||
 | 
					    let pds_endpoints = ["https://bsky.social", "https://syu.is"];
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    for pds in &pds_endpoints {
 | 
				
			||||||
 | 
					        let describe_url = format!("{}/xrpc/com.atproto.repo.describeRepo?repo={}", pds, urlencoding::encode(did));
 | 
				
			||||||
 | 
					        if let Ok(response) = client.get(&describe_url).send().await {
 | 
				
			||||||
 | 
					            if response.status().is_success() {
 | 
				
			||||||
 | 
					                if let Ok(data) = response.json::<Value>().await {
 | 
				
			||||||
 | 
					                    if let Some(services) = data["didDoc"]["service"].as_array() {
 | 
				
			||||||
 | 
					                        if let Some(pds_service) = services.iter().find(|s| 
 | 
				
			||||||
 | 
					                            s["id"] == "#atproto_pds" || s["type"] == "AtprotoPersonalDataServer"
 | 
				
			||||||
 | 
					                        ) {
 | 
				
			||||||
 | 
					                            if let Some(endpoint) = pds_service["serviceEndpoint"].as_str() {
 | 
				
			||||||
 | 
					                                return Ok(endpoint.to_string());
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Fallback to default
 | 
				
			||||||
 | 
					    Ok("https://bsky.social".to_string())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async fn update_user_list(config: &mut AuthConfig, did: &str, handle: &str) -> Result<()> {
 | 
					async fn update_user_list(config: &mut AuthConfig, did: &str, handle: &str) -> Result<()> {
 | 
				
			||||||
    // Get current user list
 | 
					    // Get current user list
 | 
				
			||||||
    let current_users = get_current_user_list(config).await?;
 | 
					    let current_users = get_current_user_list(config).await?;
 | 
				
			||||||
@@ -1130,6 +1255,68 @@ fn extract_did_from_uri(uri: &str) -> Option<String> {
 | 
				
			|||||||
    None
 | 
					    None
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// OAuth config structure for loading admin settings
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					struct OAuthConfig {
 | 
				
			||||||
 | 
					    admin: String,
 | 
				
			||||||
 | 
					    pds: Option<String>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Load OAuth config from project's config.toml
 | 
				
			||||||
 | 
					fn load_oauth_config_from_project() -> Option<OAuthConfig> {
 | 
				
			||||||
 | 
					    // Try to find config.toml in current directory or parent directories
 | 
				
			||||||
 | 
					    let mut current_dir = std::env::current_dir().ok()?;
 | 
				
			||||||
 | 
					    let mut config_path = None;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    for _ in 0..5 { // Search up to 5 levels up
 | 
				
			||||||
 | 
					        let potential_config = current_dir.join("config.toml");
 | 
				
			||||||
 | 
					        if potential_config.exists() {
 | 
				
			||||||
 | 
					            config_path = Some(potential_config);
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if !current_dir.pop() {
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let config_path = config_path?;
 | 
				
			||||||
 | 
					    let config_content = std::fs::read_to_string(&config_path).ok()?;
 | 
				
			||||||
 | 
					    let config: toml::Value = config_content.parse().ok()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let oauth_config = config.get("oauth").and_then(|v| v.as_table())?;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let admin = oauth_config
 | 
				
			||||||
 | 
					        .get("admin")
 | 
				
			||||||
 | 
					        .and_then(|v| v.as_str())?
 | 
				
			||||||
 | 
					        .to_string();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let pds = oauth_config
 | 
				
			||||||
 | 
					        .get("pds")
 | 
				
			||||||
 | 
					        .and_then(|v| v.as_str())
 | 
				
			||||||
 | 
					        .map(|s| s.to_string());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Some(OAuthConfig { admin, pds })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Resolve handle to DID using PLC directory
 | 
				
			||||||
 | 
					async fn resolve_handle_to_did(handle: &str, network_config: &NetworkConfig) -> Result<String> {
 | 
				
			||||||
 | 
					    let client = reqwest::Client::new();
 | 
				
			||||||
 | 
					    let url = format!("{}/xrpc/com.atproto.identity.resolveHandle?handle={}", 
 | 
				
			||||||
 | 
					                     network_config.bsky_api, urlencoding::encode(handle));
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let response = client.get(&url).send().await?;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if !response.status().is_success() {
 | 
				
			||||||
 | 
					        return Err(anyhow::anyhow!("Failed to resolve handle: {}", response.status()));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let data: Value = response.json().await?;
 | 
				
			||||||
 | 
					    let did = data["did"].as_str()
 | 
				
			||||||
 | 
					        .ok_or_else(|| anyhow::anyhow!("DID not found in response"))?;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    Ok(did.to_string())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn test_api() -> Result<()> {
 | 
					pub async fn test_api() -> Result<()> {
 | 
				
			||||||
    println!("{}", "🧪 Testing API access to comments collection...".cyan().bold());
 | 
					    println!("{}", "🧪 Testing API access to comments collection...".cyan().bold());
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
@@ -1452,32 +1639,23 @@ fn extract_date_from_slug(slug: &str) -> String {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async fn get_ai_profile(client: &reqwest::Client, ai_config: &AiConfig) -> Result<serde_json::Value> {
 | 
					async fn get_ai_profile(client: &reqwest::Client, ai_config: &AiConfig) -> Result<serde_json::Value> {
 | 
				
			||||||
    // Resolve AI's actual PDS first
 | 
					    let handle = &ai_config.ai_handle;
 | 
				
			||||||
    let pds_endpoints = ["https://bsky.social", "https://syu.is"];
 | 
					 | 
				
			||||||
    let mut network_config = get_network_config("bsky.social"); // Default fallback
 | 
					 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    for pds in &pds_endpoints {
 | 
					    // First, try to resolve PDS from handle using the admin's configured PDS
 | 
				
			||||||
        let describe_url = format!("{}/xrpc/com.atproto.repo.describeRepo?repo={}", pds, urlencoding::encode(&ai_config.ai_did));
 | 
					    let mut network_config = ai_config.network.clone();
 | 
				
			||||||
        if let Ok(response) = client.get(&describe_url).send().await {
 | 
					    
 | 
				
			||||||
            if response.status().is_success() {
 | 
					    // For admin/ai handles matching configured PDS, use the configured network
 | 
				
			||||||
                if let Ok(data) = response.json::<Value>().await {
 | 
					    if let Some(oauth_config) = load_oauth_config_from_project() {
 | 
				
			||||||
                    if let Some(services) = data["didDoc"]["service"].as_array() {
 | 
					        if handle == &oauth_config.admin {
 | 
				
			||||||
                        if let Some(pds_service) = services.iter().find(|s| 
 | 
					            // Use configured PDS for admin handle
 | 
				
			||||||
                            s["id"] == "#atproto_pds" || s["type"] == "AtprotoPersonalDataServer"
 | 
					            let pds = oauth_config.pds.unwrap_or_else(|| "syu.is".to_string());
 | 
				
			||||||
                        ) {
 | 
					            network_config = get_network_config(&pds);
 | 
				
			||||||
                            if let Some(endpoint) = pds_service["serviceEndpoint"].as_str() {
 | 
					 | 
				
			||||||
                                network_config = get_network_config_from_pds(endpoint);
 | 
					 | 
				
			||||||
                                break;
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    // Get profile from appropriate bsky API
 | 
				
			||||||
    let url = format!("{}/xrpc/app.bsky.actor.getProfile?actor={}", 
 | 
					    let url = format!("{}/xrpc/app.bsky.actor.getProfile?actor={}", 
 | 
				
			||||||
                     network_config.bsky_api, urlencoding::encode(&ai_config.ai_did));
 | 
					                     network_config.bsky_api, urlencoding::encode(handle));
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    let response = client
 | 
					    let response = client
 | 
				
			||||||
        .get(&url)
 | 
					        .get(&url)
 | 
				
			||||||
@@ -1485,20 +1663,41 @@ async fn get_ai_profile(client: &reqwest::Client, ai_config: &AiConfig) -> Resul
 | 
				
			|||||||
        .await?;
 | 
					        .await?;
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
    if !response.status().is_success() {
 | 
					    if !response.status().is_success() {
 | 
				
			||||||
        // Fallback to default AI profile
 | 
					        // Try to resolve DID first, then retry with DID
 | 
				
			||||||
 | 
					        match resolve_handle_to_did(handle, &network_config).await {
 | 
				
			||||||
 | 
					            Ok(resolved_did) => {
 | 
				
			||||||
 | 
					                // Retry with resolved DID
 | 
				
			||||||
 | 
					                let did_url = format!("{}/xrpc/app.bsky.actor.getProfile?actor={}", 
 | 
				
			||||||
 | 
					                                     network_config.bsky_api, urlencoding::encode(&resolved_did));
 | 
				
			||||||
 | 
					                let did_response = client.get(&did_url).send().await?;
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if did_response.status().is_success() {
 | 
				
			||||||
 | 
					                    let profile_data: serde_json::Value = did_response.json().await?;
 | 
				
			||||||
 | 
					                    return Ok(serde_json::json!({
 | 
				
			||||||
 | 
					                        "did": resolved_did,
 | 
				
			||||||
 | 
					                        "handle": profile_data["handle"].as_str().unwrap_or(handle),
 | 
				
			||||||
 | 
					                        "displayName": profile_data["displayName"].as_str().unwrap_or("ai"),
 | 
				
			||||||
 | 
					                        "avatar": profile_data["avatar"].as_str()
 | 
				
			||||||
 | 
					                    }));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            Err(_) => {}
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Final fallback to default AI profile
 | 
				
			||||||
        return Ok(serde_json::json!({
 | 
					        return Ok(serde_json::json!({
 | 
				
			||||||
            "did": ai_config.ai_did,
 | 
					            "did": ai_config.ai_did,
 | 
				
			||||||
            "handle": "yui.syui.ai",
 | 
					            "handle": handle,
 | 
				
			||||||
            "displayName": "ai",
 | 
					            "displayName": "ai",
 | 
				
			||||||
            "avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:4hqjfn7m6n5hno3doamuhgef/bafkreiaxkv624mffw3cfyi67ufxtwuwsy2mjw2ygezsvtd44ycbgkfdo2a@jpeg"
 | 
					            "avatar": format!("https://api.dicebear.com/7.x/bottts-neutral/svg?seed={}", handle)
 | 
				
			||||||
        }));
 | 
					        }));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    let profile_data: serde_json::Value = response.json().await?;
 | 
					    let profile_data: serde_json::Value = response.json().await?;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    Ok(serde_json::json!({
 | 
					    Ok(serde_json::json!({
 | 
				
			||||||
        "did": ai_config.ai_did,
 | 
					        "did": profile_data["did"].as_str().unwrap_or(&ai_config.ai_did),
 | 
				
			||||||
        "handle": profile_data["handle"].as_str().unwrap_or("yui.syui.ai"),
 | 
					        "handle": profile_data["handle"].as_str().unwrap_or(handle),
 | 
				
			||||||
        "displayName": profile_data["displayName"].as_str().unwrap_or("ai"),
 | 
					        "displayName": profile_data["displayName"].as_str().unwrap_or("ai"),
 | 
				
			||||||
        "avatar": profile_data["avatar"].as_str()
 | 
					        "avatar": profile_data["avatar"].as_str()
 | 
				
			||||||
    }))
 | 
					    }))
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,7 @@ pub struct Config {
 | 
				
			|||||||
    pub site: SiteConfig,
 | 
					    pub site: SiteConfig,
 | 
				
			||||||
    pub build: BuildConfig,
 | 
					    pub build: BuildConfig,
 | 
				
			||||||
    pub ai: Option<AiConfig>,
 | 
					    pub ai: Option<AiConfig>,
 | 
				
			||||||
 | 
					    pub oauth: Option<OAuthConfig>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
 | 
					#[derive(Debug, Serialize, Deserialize, Clone)]
 | 
				
			||||||
@@ -37,6 +38,7 @@ pub struct AiConfig {
 | 
				
			|||||||
    pub model: Option<String>,
 | 
					    pub model: Option<String>,
 | 
				
			||||||
    pub host: Option<String>,
 | 
					    pub host: Option<String>,
 | 
				
			||||||
    pub system_prompt: Option<String>,
 | 
					    pub system_prompt: Option<String>,
 | 
				
			||||||
 | 
					    pub handle: Option<String>,
 | 
				
			||||||
    pub ai_did: Option<String>,
 | 
					    pub ai_did: Option<String>,
 | 
				
			||||||
    pub api_key: Option<String>,
 | 
					    pub api_key: Option<String>,
 | 
				
			||||||
    pub gpt_endpoint: Option<String>,
 | 
					    pub gpt_endpoint: Option<String>,
 | 
				
			||||||
@@ -44,6 +46,16 @@ pub struct AiConfig {
 | 
				
			|||||||
    pub num_predict: Option<i32>,
 | 
					    pub num_predict: Option<i32>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Serialize, Deserialize, Clone)]
 | 
				
			||||||
 | 
					pub struct OAuthConfig {
 | 
				
			||||||
 | 
					    pub json: Option<String>,
 | 
				
			||||||
 | 
					    pub redirect: Option<String>,
 | 
				
			||||||
 | 
					    pub admin: Option<String>,
 | 
				
			||||||
 | 
					    pub collection: Option<String>,
 | 
				
			||||||
 | 
					    pub pds: Option<String>,
 | 
				
			||||||
 | 
					    pub handle_list: Option<Vec<String>>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
 | 
					#[derive(Debug, Serialize, Deserialize, Clone)]
 | 
				
			||||||
pub struct AtprotoConfig {
 | 
					pub struct AtprotoConfig {
 | 
				
			||||||
    pub client_id: String,
 | 
					    pub client_id: String,
 | 
				
			||||||
@@ -160,12 +172,14 @@ impl Default for Config {
 | 
				
			|||||||
                model: Some("gemma3:4b".to_string()),
 | 
					                model: Some("gemma3:4b".to_string()),
 | 
				
			||||||
                host: None,
 | 
					                host: None,
 | 
				
			||||||
                system_prompt: Some("You are a helpful AI assistant trained on this blog's content.".to_string()),
 | 
					                system_prompt: Some("You are a helpful AI assistant trained on this blog's content.".to_string()),
 | 
				
			||||||
 | 
					                handle: None,
 | 
				
			||||||
                ai_did: None,
 | 
					                ai_did: None,
 | 
				
			||||||
                api_key: None,
 | 
					                api_key: None,
 | 
				
			||||||
                gpt_endpoint: None,
 | 
					                gpt_endpoint: None,
 | 
				
			||||||
                atproto_config: None,
 | 
					                atproto_config: None,
 | 
				
			||||||
                num_predict: None,
 | 
					                num_predict: None,
 | 
				
			||||||
            }),
 | 
					            }),
 | 
				
			||||||
 | 
					            oauth: None,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										33
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								src/main.rs
									
									
									
									
									
								
							@@ -102,7 +102,23 @@ enum Commands {
 | 
				
			|||||||
#[derive(Subcommand)]
 | 
					#[derive(Subcommand)]
 | 
				
			||||||
enum AuthCommands {
 | 
					enum AuthCommands {
 | 
				
			||||||
    /// Initialize OAuth authentication
 | 
					    /// Initialize OAuth authentication
 | 
				
			||||||
    Init,
 | 
					    Init {
 | 
				
			||||||
 | 
					        /// Specify PDS server (e.g., syu.is, bsky.social)
 | 
				
			||||||
 | 
					        #[arg(long)]
 | 
				
			||||||
 | 
					        pds: Option<String>,
 | 
				
			||||||
 | 
					        /// Handle/username for authentication
 | 
				
			||||||
 | 
					        #[arg(long)]
 | 
				
			||||||
 | 
					        handle: Option<String>,
 | 
				
			||||||
 | 
					        /// Use password authentication instead of JWT
 | 
				
			||||||
 | 
					        #[arg(long)]
 | 
				
			||||||
 | 
					        password: bool,
 | 
				
			||||||
 | 
					        /// Access JWT token (alternative to password auth)
 | 
				
			||||||
 | 
					        #[arg(long)]
 | 
				
			||||||
 | 
					        access_jwt: Option<String>,
 | 
				
			||||||
 | 
					        /// Refresh JWT token (required with access-jwt)
 | 
				
			||||||
 | 
					        #[arg(long)]
 | 
				
			||||||
 | 
					        refresh_jwt: Option<String>,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    /// Show current authentication status
 | 
					    /// Show current authentication status
 | 
				
			||||||
    Status,
 | 
					    Status,
 | 
				
			||||||
    /// Logout and clear credentials
 | 
					    /// Logout and clear credentials
 | 
				
			||||||
@@ -122,6 +138,14 @@ enum StreamCommands {
 | 
				
			|||||||
        #[arg(long)]
 | 
					        #[arg(long)]
 | 
				
			||||||
        ai_generate: bool,
 | 
					        ai_generate: bool,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    /// Initialize user list for admin account
 | 
				
			||||||
 | 
					    Init {
 | 
				
			||||||
 | 
					        /// Path to the blog project directory
 | 
				
			||||||
 | 
					        project_dir: Option<PathBuf>,
 | 
				
			||||||
 | 
					        /// Handles to add to initial user list (comma-separated)
 | 
				
			||||||
 | 
					        #[arg(long)]
 | 
				
			||||||
 | 
					        handles: Option<String>,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    /// Stop monitoring
 | 
					    /// Stop monitoring
 | 
				
			||||||
    Stop,
 | 
					    Stop,
 | 
				
			||||||
    /// Show monitoring status
 | 
					    /// Show monitoring status
 | 
				
			||||||
@@ -183,8 +207,8 @@ async fn main() -> Result<()> {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        Commands::Auth { command } => {
 | 
					        Commands::Auth { command } => {
 | 
				
			||||||
            match command {
 | 
					            match command {
 | 
				
			||||||
                AuthCommands::Init => {
 | 
					                AuthCommands::Init { pds, handle, password, access_jwt, refresh_jwt } => {
 | 
				
			||||||
                    commands::auth::init().await?;
 | 
					                    commands::auth::init_with_options(pds, handle, password, access_jwt, refresh_jwt).await?;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                AuthCommands::Status => {
 | 
					                AuthCommands::Status => {
 | 
				
			||||||
                    commands::auth::status().await?;
 | 
					                    commands::auth::status().await?;
 | 
				
			||||||
@@ -199,6 +223,9 @@ async fn main() -> Result<()> {
 | 
				
			|||||||
                StreamCommands::Start { project_dir, daemon, ai_generate } => {
 | 
					                StreamCommands::Start { project_dir, daemon, ai_generate } => {
 | 
				
			||||||
                    commands::stream::start(project_dir, daemon, ai_generate).await?;
 | 
					                    commands::stream::start(project_dir, daemon, ai_generate).await?;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					                StreamCommands::Init { project_dir, handles } => {
 | 
				
			||||||
 | 
					                    commands::stream::init_user_list(project_dir, handles).await?;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
                StreamCommands::Stop => {
 | 
					                StreamCommands::Stop => {
 | 
				
			||||||
                    commands::stream::stop().await?;
 | 
					                    commands::stream::stop().await?;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user