Files
codetyper.cli/src/services/brain.ts
2026-02-04 21:32:30 -05:00

701 lines
16 KiB
TypeScript

/**
* 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<VarsFile> => {
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<void> => {
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<BrainCredentials | null> => {
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<void> => {
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<void> => {
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<string | undefined> => {
// 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<string | undefined> => {
const vars = await loadVarsFile();
return vars.brainJwtToken;
};
/**
* Set API key in vars file
*/
export const setApiKey = async (apiKey: string): Promise<void> => {
const vars = await loadVarsFile();
await saveVarsFile({ ...vars, brainApiKey: apiKey });
};
/**
* Set JWT token in vars file
*/
export const setJwtToken = async (jwtToken: string): Promise<void> => {
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<void> => {
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<string | undefined> => {
const apiKey = await getApiKey();
if (apiKey) {
return apiKey;
}
return getJwtToken();
};
/**
* Check if Brain service is available and connect
*/
export const connect = async (): Promise<boolean> => {
// 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<BrainRecallResponse | null> => {
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<string | null> => {
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<BrainConcept | null> => {
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<BrainExtractResponse | null> => {
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<boolean> => {
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<BrainState>): 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<boolean> => {
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<boolean> => {
const hasAuth = await isAuthenticated();
if (hasAuth) {
return connect();
}
return false;
};