/** * Brain Service * * Business logic layer for the CodeTyper Brain integration. * Provides context injection, knowledge recall, and learning capabilities. */ import fs from "fs/promises"; import { DIRS, FILES } from "@constants/paths"; import { BRAIN_DEFAULTS, BRAIN_ERRORS, BRAIN_DISABLED } from "@constants/brain"; import * as brainApi from "@api/brain"; import type { BrainCredentials, BrainState, BrainConnectionStatus, BrainUser, BrainConcept, BrainRecallResponse, BrainExtractResponse, } from "@/types/brain"; // ============================================================================ // State Management (Singleton via Closure) // ============================================================================ interface VarsFile { brainApiKey?: string; brainJwtToken?: string; } let brainState: BrainState = { status: "disconnected", user: null, projectId: BRAIN_DEFAULTS.PROJECT_ID, knowledgeCount: 0, memoryCount: 0, lastError: null, }; let cachedCredentials: BrainCredentials | null = null; let cachedVars: VarsFile | null = null; // ============================================================================ // Vars File Management // ============================================================================ /** * Load vars file from disk */ const loadVarsFile = async (): Promise => { if (cachedVars) { return cachedVars; } try { const data = await fs.readFile(FILES.vars, "utf-8"); cachedVars = JSON.parse(data) as VarsFile; return cachedVars; } catch { return {}; } }; /** * Save vars file to disk */ const saveVarsFile = async (vars: VarsFile): Promise => { try { await fs.mkdir(DIRS.config, { recursive: true }); await fs.writeFile(FILES.vars, JSON.stringify(vars, null, 2), "utf-8"); cachedVars = vars; } catch (error) { throw new Error(`Failed to save vars file: ${error}`); } }; // ============================================================================ // Credentials Management // ============================================================================ /** * Get path to brain credentials file */ const getCredentialsPath = (): string => { return `${DIRS.data}/brain-credentials.json`; }; /** * Load brain credentials from disk */ export const loadCredentials = async (): Promise => { if (cachedCredentials) { return cachedCredentials; } try { const data = await fs.readFile(getCredentialsPath(), "utf-8"); cachedCredentials = JSON.parse(data) as BrainCredentials; return cachedCredentials; } catch { return null; } }; /** * Save brain credentials to disk */ export const saveCredentials = async ( credentials: BrainCredentials, ): Promise => { try { await fs.mkdir(DIRS.data, { recursive: true }); await fs.writeFile( getCredentialsPath(), JSON.stringify(credentials, null, 2), "utf-8", ); cachedCredentials = credentials; } catch (error) { throw new Error(`Failed to save brain credentials: ${error}`); } }; /** * Clear brain credentials */ export const clearCredentials = async (): Promise => { try { await fs.unlink(getCredentialsPath()); cachedCredentials = null; } catch { // File may not exist, ignore } // Also clear vars file entries try { const vars = await loadVarsFile(); await saveVarsFile({ ...vars, brainApiKey: undefined, brainJwtToken: undefined, }); } catch { // Ignore errors } }; /** * Get API key from vars file or environment */ export const getApiKey = async (): Promise => { // First check environment variable const envKey = process.env.CODETYPER_BRAIN_API_KEY; if (envKey) { return envKey; } // Then check vars file const vars = await loadVarsFile(); return vars.brainApiKey; }; /** * Get JWT token from vars file */ export const getJwtToken = async (): Promise => { const vars = await loadVarsFile(); return vars.brainJwtToken; }; /** * Set API key in vars file */ export const setApiKey = async (apiKey: string): Promise => { const vars = await loadVarsFile(); await saveVarsFile({ ...vars, brainApiKey: apiKey }); }; /** * Set JWT token in vars file */ export const setJwtToken = async (jwtToken: string): Promise => { const vars = await loadVarsFile(); await saveVarsFile({ ...vars, brainJwtToken: jwtToken }); }; // ============================================================================ // Authentication // ============================================================================ /** * Login to Brain service */ export const login = async ( email: string, password: string, ): Promise<{ success: boolean; user?: BrainUser; error?: string }> => { try { updateState({ status: "connecting" }); const response = await brainApi.login(email, password); if (response.success && response.data) { const credentials: BrainCredentials = { accessToken: response.data.access_token, refreshToken: response.data.refresh_token, expiresAt: response.data.expires_at, user: response.data.user, }; await saveCredentials(credentials); updateState({ status: "connected", user: response.data.user, lastError: null, }); return { success: true, user: response.data.user }; } updateState({ status: "error", lastError: "Login failed" }); return { success: false, error: "Login failed" }; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; updateState({ status: "error", lastError: errorMessage }); return { success: false, error: errorMessage }; } }; /** * Register a new account */ export const register = async ( email: string, password: string, displayName: string, ): Promise<{ success: boolean; user?: BrainUser; error?: string }> => { try { updateState({ status: "connecting" }); const response = await brainApi.register(email, password, displayName); if (response.success && response.data) { const credentials: BrainCredentials = { accessToken: response.data.access_token, refreshToken: response.data.refresh_token, expiresAt: response.data.expires_at, user: response.data.user, }; await saveCredentials(credentials); updateState({ status: "connected", user: response.data.user, lastError: null, }); return { success: true, user: response.data.user }; } updateState({ status: "error", lastError: "Registration failed" }); return { success: false, error: "Registration failed" }; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; updateState({ status: "error", lastError: errorMessage }); return { success: false, error: errorMessage }; } }; /** * Logout from Brain service */ export const logout = async (): Promise => { try { const credentials = await loadCredentials(); if (credentials?.refreshToken) { await brainApi.logout(credentials.refreshToken); } } catch { // Ignore logout errors } finally { await clearCredentials(); updateState({ status: "disconnected", user: null, knowledgeCount: 0, memoryCount: 0, }); } }; // ============================================================================ // Connection Management // ============================================================================ /** * Get authentication token (API key or JWT token) */ export const getAuthToken = async (): Promise => { const apiKey = await getApiKey(); if (apiKey) { return apiKey; } return getJwtToken(); }; /** * Check if Brain service is available and connect */ export const connect = async (): Promise => { // Skip connection when Brain is disabled if (BRAIN_DISABLED) { return false; } try { updateState({ status: "connecting" }); // First check if service is healthy await brainApi.checkHealth(); // Then check if we have valid credentials (API key or JWT token) const authToken = await getAuthToken(); if (!authToken) { updateState({ status: "disconnected", lastError: null }); return false; } // Try to get stats to verify credentials are valid const projectId = brainState.projectId ?? BRAIN_DEFAULTS.PROJECT_ID; const statsResponse = await brainApi.getKnowledgeStats( projectId, authToken, ); if (statsResponse.success && statsResponse.data) { updateState({ status: "connected", knowledgeCount: statsResponse.data.total_concepts, lastError: null, }); // Also try to get memory stats try { const memoryStats = await brainApi.getMemoryStats(authToken); updateState({ memoryCount: memoryStats.totalNodes }); } catch { // Memory stats are optional } return true; } updateState({ status: "error", lastError: BRAIN_ERRORS.INVALID_API_KEY }); return false; } catch (error) { const errorMessage = error instanceof Error ? error.message : BRAIN_ERRORS.CONNECTION_FAILED; updateState({ status: "error", lastError: errorMessage }); return false; } }; /** * Disconnect from Brain service */ export const disconnect = (): void => { updateState({ status: "disconnected", knowledgeCount: 0, memoryCount: 0, lastError: null, }); }; /** * Check if connected to Brain */ export const isConnected = (): boolean => { if (BRAIN_DISABLED) return false; return brainState.status === "connected"; }; // ============================================================================ // Knowledge Operations // ============================================================================ /** * Recall relevant knowledge for a query */ export const recall = async ( query: string, limit = 5, ): Promise => { if (!isConnected()) { return null; } try { const apiKey = await getApiKey(); if (!apiKey) { return null; } const response = await brainApi.recallKnowledge( { query, project_id: brainState.projectId ?? BRAIN_DEFAULTS.PROJECT_ID, limit, }, apiKey, ); return response; } catch (error) { const errorMessage = error instanceof Error ? error.message : BRAIN_ERRORS.RECALL_FAILED; updateState({ lastError: errorMessage }); return null; } }; /** * Get context string for prompt injection */ export const getContext = async ( query: string, maxConcepts = 3, ): Promise => { if (!isConnected()) { return null; } try { const apiKey = await getApiKey(); if (!apiKey) { return null; } const response = await brainApi.buildContext( { query, project_id: brainState.projectId ?? BRAIN_DEFAULTS.PROJECT_ID, max_concepts: maxConcepts, }, apiKey, ); if (response.success && response.data.has_knowledge) { return response.data.context; } return null; } catch { return null; } }; /** * Learn a concept */ export const learn = async ( name: string, whatItDoes: string, options?: { howItWorks?: string; patterns?: string[]; files?: string[]; keyFunctions?: string[]; aliases?: string[]; }, ): Promise => { if (!isConnected()) { return null; } try { const apiKey = await getApiKey(); if (!apiKey) { return null; } const response = await brainApi.learnConcept( { project_id: brainState.projectId ?? BRAIN_DEFAULTS.PROJECT_ID, name, what_it_does: whatItDoes, how_it_works: options?.howItWorks, patterns: options?.patterns, files: options?.files, key_functions: options?.keyFunctions, aliases: options?.aliases, }, apiKey, ); if (response.success && response.data) { // Update knowledge count updateState({ knowledgeCount: brainState.knowledgeCount + 1 }); return response.data; } return null; } catch (error) { const errorMessage = error instanceof Error ? error.message : BRAIN_ERRORS.LEARN_FAILED; updateState({ lastError: errorMessage }); return null; } }; /** * Extract and learn concepts from content */ export const extractAndLearn = async ( content: string, source = "conversation", ): Promise => { if (!isConnected()) { return null; } try { const apiKey = await getApiKey(); if (!apiKey) { return null; } const response = await brainApi.extractConcepts( { content, project_id: brainState.projectId ?? BRAIN_DEFAULTS.PROJECT_ID, source, }, apiKey, ); if (response.success) { // Update knowledge count const newCount = brainState.knowledgeCount + response.data.stored + response.data.updated; updateState({ knowledgeCount: newCount }); return response; } return null; } catch (error) { const errorMessage = error instanceof Error ? error.message : BRAIN_ERRORS.EXTRACT_FAILED; updateState({ lastError: errorMessage }); return null; } }; // ============================================================================ // Memory Operations // ============================================================================ /** * Search memories */ export const searchMemories = async ( query: string, limit = 10, ): Promise<{ memories: Array<{ content: string; similarity: number }>; } | null> => { if (!isConnected()) { return null; } try { const apiKey = await getApiKey(); if (!apiKey) { return null; } const response = await brainApi.searchMemories( { query, limit, project_id: brainState.projectId ?? BRAIN_DEFAULTS.PROJECT_ID, }, apiKey, ); return { memories: response.memories.map((m) => ({ content: m.content, similarity: m.similarity ?? 0, })), }; } catch { return null; } }; /** * Store a memory */ export const storeMemory = async ( content: string, type: | "fact" | "pattern" | "correction" | "preference" | "context" = "context", ): Promise => { if (!isConnected()) { return false; } try { const apiKey = await getApiKey(); if (!apiKey) { return false; } const response = await brainApi.storeMemory( { content, type, project_id: brainState.projectId ?? BRAIN_DEFAULTS.PROJECT_ID, }, apiKey, ); if (response.success) { updateState({ memoryCount: brainState.memoryCount + 1 }); return true; } return false; } catch { return false; } }; // ============================================================================ // State Accessors // ============================================================================ /** * Get current brain state */ export const getState = (): BrainState => { return { ...brainState }; }; /** * Update brain state */ const updateState = (updates: Partial): void => { brainState = { ...brainState, ...updates }; }; /** * Set project ID */ export const setProjectId = (projectId: number): void => { updateState({ projectId }); }; /** * Get connection status */ export const getStatus = (): BrainConnectionStatus => { return brainState.status; }; /** * Check if authenticated (has API key or JWT token) */ export const isAuthenticated = async (): Promise => { const apiKey = await getApiKey(); const jwtToken = await getJwtToken(); return apiKey !== undefined || jwtToken !== undefined; }; // ============================================================================ // Initialization // ============================================================================ /** * Initialize brain service (auto-connect if credentials available) */ export const initialize = async (): Promise => { const hasAuth = await isAuthenticated(); if (hasAuth) { return connect(); } return false; };