arela
Version:
AI-powered CTO with multi-agent orchestration, code summarization, visual testing (web + mobile) for blazing fast development.
500 lines (493 loc) • 16.7 kB
JavaScript
import path from "node:path";
import os from "node:os";
import fs from "fs-extra";
import Database from "better-sqlite3";
import { randomUUID } from "node:crypto";
/**
* User Memory Layer (Layer 6 - Hexi-Memory)
*
* Stores long-term context about the user across ALL projects:
* - User preferences and coding style
* - Expertise levels across different domains
* - Learned patterns from all projects
* - Global conventions
* - Project history
* - Arbitrary metadata
*
* Features:
* - Global SQLite database (~/.arela/user.db)
* - Pattern learning across projects
* - Project history tracking
* - Top patterns by frequency
* - Recent projects query
* - Fast queries (<100ms)
*
* Lifespan: Forever (until explicitly deleted)
*/
export class UserMemory {
db;
dbPath;
userId;
initialized = false;
constructor() {
// Global user database at ~/.arela/user.db
const homeDir = os.homedir();
this.dbPath = path.join(homeDir, ".arela", "user.db");
}
/**
* Initialize user memory
* - Sets up global SQLite database
* - Creates tables if needed
* - Loads or creates user record
*/
async init() {
if (this.initialized) {
return;
}
// Ensure directory exists
await fs.ensureDir(path.dirname(this.dbPath));
// Open database
this.db = new Database(this.dbPath);
this.db.pragma("journal_mode = WAL"); // Better concurrency
// Create tables if they don't exist
this.createTables();
// Get or create user record
await this.initUser();
this.initialized = true;
}
/**
* Create database tables
*/
createTables() {
if (!this.db) {
throw new Error("Database not initialized");
}
this.db.exec(`
CREATE TABLE IF NOT EXISTS user_info (
id TEXT PRIMARY KEY,
created_at INTEGER DEFAULT (strftime('%s', 'now')),
updated_at INTEGER DEFAULT (strftime('%s', 'now'))
);
CREATE TABLE IF NOT EXISTS preferences (
user_id TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES user_info(id),
PRIMARY KEY (user_id, key)
);
CREATE TABLE IF NOT EXISTS expertise (
user_id TEXT NOT NULL,
domain TEXT NOT NULL,
level TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES user_info(id),
PRIMARY KEY (user_id, domain)
);
CREATE TABLE IF NOT EXISTS user_patterns (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
description TEXT,
frequency INTEGER DEFAULT 0,
examples TEXT,
learned_from TEXT,
FOREIGN KEY (user_id) REFERENCES user_info(id)
);
CREATE TABLE IF NOT EXISTS global_conventions (
user_id TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES user_info(id),
PRIMARY KEY (user_id, key)
);
CREATE TABLE IF NOT EXISTS project_history (
user_id TEXT NOT NULL,
project_id TEXT NOT NULL,
project_path TEXT NOT NULL,
last_accessed INTEGER DEFAULT (strftime('%s', 'now')),
total_sessions INTEGER DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES user_info(id),
PRIMARY KEY (user_id, project_id)
);
CREATE TABLE IF NOT EXISTS user_metadata (
user_id TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES user_info(id),
PRIMARY KEY (user_id, key)
);
-- Indexes for better query performance
CREATE INDEX IF NOT EXISTS idx_patterns_frequency ON user_patterns(frequency DESC);
CREATE INDEX IF NOT EXISTS idx_project_history_accessed ON project_history(last_accessed DESC);
`);
}
/**
* Initialize or load user record
*/
async initUser() {
if (!this.db) {
throw new Error("Database not initialized");
}
// Check if user exists (there should only be one user)
const existing = this.db
.prepare("SELECT id FROM user_info LIMIT 1")
.get();
if (existing) {
this.userId = existing.id;
}
else {
// Create new user record
this.userId = randomUUID();
this.db
.prepare("INSERT INTO user_info (id) VALUES (?)")
.run(this.userId);
}
}
/**
* Get current user ID
*/
getUserId() {
if (!this.userId) {
throw new Error("User not initialized. Call init() first.");
}
return this.userId;
}
// ===== Preferences Methods =====
/**
* Set a user preference
*/
async setPreference(key, value) {
if (!this.db || !this.userId) {
throw new Error("User not initialized. Call init() first.");
}
this.db
.prepare(`INSERT INTO preferences (user_id, key, value)
VALUES (?, ?, ?)
ON CONFLICT(user_id, key) DO UPDATE SET value = excluded.value`)
.run(this.userId, key, value);
}
/**
* Get a preference value
*/
async getPreference(key) {
if (!this.db || !this.userId) {
throw new Error("User not initialized. Call init() first.");
}
const result = this.db
.prepare("SELECT value FROM preferences WHERE user_id = ? AND key = ?")
.get(this.userId, key);
return result?.value;
}
/**
* Get all preferences
*/
async getAllPreferences() {
if (!this.db || !this.userId) {
throw new Error("User not initialized. Call init() first.");
}
const results = this.db
.prepare("SELECT key, value FROM preferences WHERE user_id = ?")
.all(this.userId);
const preferences = {};
for (const row of results) {
preferences[row.key] = row.value;
}
return preferences;
}
// ===== Expertise Methods =====
/**
* Set expertise level for a domain
*/
async setExpertise(domain, level) {
if (!this.db || !this.userId) {
throw new Error("User not initialized. Call init() first.");
}
this.db
.prepare(`INSERT INTO expertise (user_id, domain, level)
VALUES (?, ?, ?)
ON CONFLICT(user_id, domain) DO UPDATE SET level = excluded.level`)
.run(this.userId, domain, level);
}
/**
* Get expertise level for a domain
*/
async getExpertise(domain) {
if (!this.db || !this.userId) {
throw new Error("User not initialized. Call init() first.");
}
const result = this.db
.prepare("SELECT level FROM expertise WHERE user_id = ? AND domain = ?")
.get(this.userId, domain);
return result?.level;
}
/**
* Get all expertise levels
*/
async getAllExpertise() {
if (!this.db || !this.userId) {
throw new Error("User not initialized. Call init() first.");
}
const results = this.db
.prepare("SELECT domain, level FROM expertise WHERE user_id = ?")
.all(this.userId);
const expertise = {};
for (const row of results) {
expertise[row.domain] = row.level;
}
return expertise;
}
// ===== Pattern Methods =====
/**
* Add a user pattern
*/
async addPattern(pattern) {
if (!this.db || !this.userId) {
throw new Error("User not initialized. Call init() first.");
}
const id = pattern.id || randomUUID();
const examples = JSON.stringify(pattern.examples);
const learnedFrom = JSON.stringify(pattern.learnedFrom);
this.db
.prepare(`INSERT INTO user_patterns (id, user_id, name, description, frequency, examples, learned_from)
VALUES (?, ?, ?, ?, ?, ?, ?)`)
.run(id, this.userId, pattern.name, pattern.description, pattern.frequency, examples, learnedFrom);
}
/**
* Get all patterns
*/
async getPatterns() {
if (!this.db || !this.userId) {
throw new Error("User not initialized. Call init() first.");
}
const results = this.db
.prepare("SELECT * FROM user_patterns WHERE user_id = ? ORDER BY frequency DESC")
.all(this.userId);
return results.map((row) => ({
id: row.id,
name: row.name,
description: row.description,
frequency: row.frequency,
examples: JSON.parse(row.examples),
learnedFrom: JSON.parse(row.learned_from),
}));
}
/**
* Increment pattern usage and add project to learned_from
*/
async incrementPatternUsage(patternId, projectId) {
if (!this.db || !this.userId) {
throw new Error("User not initialized. Call init() first.");
}
// Get current pattern
const pattern = this.db
.prepare("SELECT learned_from FROM user_patterns WHERE id = ? AND user_id = ?")
.get(patternId, this.userId);
if (!pattern) {
return; // Pattern doesn't exist
}
const learnedFrom = JSON.parse(pattern.learned_from);
// Add projectId if not already present
if (!learnedFrom.includes(projectId)) {
learnedFrom.push(projectId);
}
// Update pattern
this.db
.prepare(`UPDATE user_patterns
SET frequency = frequency + 1, learned_from = ?
WHERE id = ? AND user_id = ?`)
.run(JSON.stringify(learnedFrom), patternId, this.userId);
}
/**
* Get top patterns by frequency
*/
async getTopPatterns(limit) {
if (!this.db || !this.userId) {
throw new Error("User not initialized. Call init() first.");
}
const results = this.db
.prepare("SELECT * FROM user_patterns WHERE user_id = ? ORDER BY frequency DESC LIMIT ?")
.all(this.userId, limit);
return results.map((row) => ({
id: row.id,
name: row.name,
description: row.description,
frequency: row.frequency,
examples: JSON.parse(row.examples),
learnedFrom: JSON.parse(row.learned_from),
}));
}
// ===== Global Conventions Methods =====
/**
* Set a global convention
*/
async setConvention(key, value) {
if (!this.db || !this.userId) {
throw new Error("User not initialized. Call init() first.");
}
this.db
.prepare(`INSERT INTO global_conventions (user_id, key, value)
VALUES (?, ?, ?)
ON CONFLICT(user_id, key) DO UPDATE SET value = excluded.value`)
.run(this.userId, key, value);
}
/**
* Get a convention value
*/
async getConvention(key) {
if (!this.db || !this.userId) {
throw new Error("User not initialized. Call init() first.");
}
const result = this.db
.prepare("SELECT value FROM global_conventions WHERE user_id = ? AND key = ?")
.get(this.userId, key);
return result?.value;
}
/**
* Get all conventions
*/
async getAllConventions() {
if (!this.db || !this.userId) {
throw new Error("User not initialized. Call init() first.");
}
const results = this.db
.prepare("SELECT key, value FROM global_conventions WHERE user_id = ?")
.all(this.userId);
const conventions = {};
for (const row of results) {
conventions[row.key] = row.value;
}
return conventions;
}
// ===== Project History Methods =====
/**
* Track a project (adds or updates project history)
*/
async trackProject(projectId, projectPath) {
if (!this.db || !this.userId) {
throw new Error("User not initialized. Call init() first.");
}
this.db
.prepare(`INSERT INTO project_history (user_id, project_id, project_path, last_accessed, total_sessions)
VALUES (?, ?, ?, ?, 1)
ON CONFLICT(user_id, project_id) DO UPDATE SET
last_accessed = ?,
project_path = excluded.project_path`)
.run(this.userId, projectId, projectPath, Date.now(), Date.now());
}
/**
* Get recent projects
*/
async getRecentProjects(limit) {
if (!this.db || !this.userId) {
throw new Error("User not initialized. Call init() first.");
}
const results = this.db
.prepare(`SELECT project_id, project_path, last_accessed, total_sessions
FROM project_history
WHERE user_id = ?
ORDER BY last_accessed DESC
LIMIT ?`)
.all(this.userId, limit);
return results.map((row) => ({
projectId: row.project_id,
projectPath: row.project_path,
lastAccessed: row.last_accessed,
totalSessions: row.total_sessions,
}));
}
/**
* Increment session count for a project
*/
async incrementSessionCount(projectId) {
if (!this.db || !this.userId) {
throw new Error("User not initialized. Call init() first.");
}
this.db
.prepare(`UPDATE project_history
SET total_sessions = total_sessions + 1, last_accessed = ?
WHERE user_id = ? AND project_id = ?`)
.run(Date.now(), this.userId, projectId);
}
// ===== Metadata Methods =====
/**
* Set metadata value
*/
async setMetadata(key, value) {
if (!this.db || !this.userId) {
throw new Error("User not initialized. Call init() first.");
}
const jsonValue = JSON.stringify(value);
this.db
.prepare(`INSERT INTO user_metadata (user_id, key, value)
VALUES (?, ?, ?)
ON CONFLICT(user_id, key) DO UPDATE SET value = excluded.value`)
.run(this.userId, key, jsonValue);
}
/**
* Get metadata value
*/
async getMetadata(key) {
if (!this.db || !this.userId) {
throw new Error("User not initialized. Call init() first.");
}
const result = this.db
.prepare("SELECT value FROM user_metadata WHERE user_id = ? AND key = ?")
.get(this.userId, key);
if (!result) {
return undefined;
}
try {
return JSON.parse(result.value);
}
catch {
return result.value;
}
}
// ===== Stats & Utility Methods =====
/**
* Get user statistics
*/
async getStats() {
if (!this.db || !this.userId) {
throw new Error("User not initialized. Call init() first.");
}
const preferencesCount = this.db
.prepare("SELECT COUNT(*) as count FROM preferences WHERE user_id = ?")
.get(this.userId);
const expertiseCount = this.db
.prepare("SELECT COUNT(*) as count FROM expertise WHERE user_id = ?")
.get(this.userId);
const patternsCount = this.db
.prepare("SELECT COUNT(*) as count FROM user_patterns WHERE user_id = ?")
.get(this.userId);
const conventionsCount = this.db
.prepare("SELECT COUNT(*) as count FROM global_conventions WHERE user_id = ?")
.get(this.userId);
const projectHistoryCount = this.db
.prepare("SELECT COUNT(*) as count FROM project_history WHERE user_id = ?")
.get(this.userId);
const metadataCount = this.db
.prepare("SELECT COUNT(*) as count FROM user_metadata WHERE user_id = ?")
.get(this.userId);
return {
userId: this.userId,
preferencesCount: preferencesCount.count,
expertiseCount: expertiseCount.count,
patternsCount: patternsCount.count,
globalConventionsCount: conventionsCount.count,
projectHistoryCount: projectHistoryCount.count,
metadataCount: metadataCount.count,
dbPath: this.dbPath,
};
}
/**
* Close database connection
*/
close() {
if (this.db) {
this.db.close();
this.db = undefined;
}
this.initialized = false;
}
}
//# sourceMappingURL=user.js.map