701 lines
16 KiB
TypeScript
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;
|
|
};
|