@justinechang39/maki
Version:
AI-powered CLI agent for file operations, CSV manipulation, todo management, and web content fetching using OpenRouter
214 lines (213 loc) • 7.99 kB
JavaScript
import { PrismaClient } from '@prisma/client';
import fs from 'fs';
import { CONFIG_DIRECTORY, DATABASE_PATH } from './config.js';
// Ensure config directory exists
if (!fs.existsSync(CONFIG_DIRECTORY)) {
fs.mkdirSync(CONFIG_DIRECTORY, { recursive: true });
}
// Set DATABASE_URL for Prisma
process.env.DATABASE_URL = `file:${DATABASE_PATH}`;
export const prisma = new PrismaClient();
// Track if database is initialized
let databaseInitialized = false;
// Initialize database schema automatically
async function initializeDatabase() {
if (databaseInitialized)
return;
try {
// Enable foreign key constraints
await prisma.$executeRaw `PRAGMA foreign_keys = ON;`;
// Try to query existing tables to see if schema exists
try {
await prisma.thread.findFirst();
databaseInitialized = true;
console.log('Database connection established!');
return;
}
catch (schemaError) {
// Schema doesn't exist, apply it using the bundled migration
console.log('Setting up database schema...');
await applyMigration();
// Verify the schema was created successfully
try {
// Small delay to ensure schema is fully applied
await new Promise(resolve => setTimeout(resolve, 100));
await prisma.thread.findFirst();
databaseInitialized = true;
console.log('Database initialized successfully!');
}
catch (verifyError) {
console.error('Schema creation failed verification:', verifyError);
// Try one more time with a direct table check
try {
await prisma.$queryRaw `SELECT name FROM sqlite_master WHERE type='table' AND name='threads'`;
console.log('Tables exist but Prisma client needs regeneration');
databaseInitialized = true;
console.log('Database initialized successfully!');
}
catch (fallbackError) {
console.error('Fallback verification also failed:', fallbackError);
throw new Error('Database schema creation failed');
}
}
}
}
catch (error) {
console.error('Failed to initialize database:', error);
throw error;
}
}
// Apply the database migration (this is the same SQL that Prisma would generate)
// This is a one-time bootstrap, after which we use pure Prisma methods
async function applyMigration() {
try {
console.log('Creating threads table...');
await prisma.$executeRawUnsafe(`
CREATE TABLE "threads" (
"id" TEXT NOT NULL PRIMARY KEY,
"title" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
)
`);
console.log('Creating messages table...');
await prisma.$executeRawUnsafe(`
CREATE TABLE "messages" (
"id" TEXT NOT NULL PRIMARY KEY,
"threadId" TEXT NOT NULL,
"role" TEXT NOT NULL CHECK ("role" IN ('USER', 'ASSISTANT', 'SYSTEM', 'TOOL')),
"content" TEXT NOT NULL,
"toolCalls" TEXT,
"toolResponses" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "messages_threadId_fkey" FOREIGN KEY ("threadId") REFERENCES "threads" ("id") ON DELETE CASCADE ON UPDATE CASCADE
)
`);
console.log('Creating indexes...');
await prisma.$executeRawUnsafe(`CREATE INDEX "messages_threadId_idx" ON "messages"("threadId")`);
await prisma.$executeRawUnsafe(`CREATE INDEX "threads_updatedAt_idx" ON "threads"("updatedAt")`);
console.log('Schema creation completed, refreshing Prisma client...');
// Force Prisma to refresh its schema understanding
await prisma.$disconnect();
await prisma.$connect();
console.log('Prisma client refreshed');
}
catch (error) {
console.error('Error during schema creation:', error);
throw error;
}
}
export class ThreadDatabase {
static async createThread(title) {
await initializeDatabase();
const thread = await prisma.thread.create({
data: {
title: title || undefined
}
});
return thread.id;
}
static async getAllThreads() {
await initializeDatabase();
const threads = await prisma.thread.findMany({
include: {
_count: {
select: { messages: true }
}
},
orderBy: { updatedAt: 'desc' }
});
// console.log(`Found ${threads.length} threads in database:`, threads.map(t => ({ id: t.id, title: t.title })))
return threads.map(thread => ({
id: thread.id,
title: thread.title || undefined,
createdAt: thread.createdAt,
messageCount: thread._count.messages
}));
}
static async getThread(threadId) {
await initializeDatabase();
const thread = await prisma.thread.findUnique({
where: { id: threadId },
include: {
messages: {
orderBy: { createdAt: 'asc' }
}
}
});
if (!thread)
return null;
return {
id: thread.id,
title: thread.title || undefined,
createdAt: thread.createdAt,
updatedAt: thread.updatedAt,
messages: thread.messages.map(msg => ({
id: msg.id,
role: msg.role,
content: msg.content,
toolCalls: msg.toolCalls ? msg.toolCalls : undefined,
toolResponses: msg.toolResponses
? msg.toolResponses
: undefined,
createdAt: msg.createdAt
}))
};
}
static async addMessage(threadId, role, content, toolCalls, toolResponses) {
await initializeDatabase();
await prisma.message.create({
data: {
threadId,
role,
content,
toolCalls: toolCalls || undefined,
toolResponses: toolResponses || undefined
}
});
// Update thread's updatedAt timestamp
await prisma.thread.update({
where: { id: threadId },
data: { updatedAt: new Date() }
});
}
static async updateThreadTitle(threadId, title) {
await initializeDatabase();
await prisma.thread.update({
where: { id: threadId },
data: {
title,
updatedAt: new Date()
}
});
}
static async deleteThread(threadId) {
await initializeDatabase();
try {
// First check if the thread exists
const existingThread = await prisma.thread.findUnique({
where: { id: threadId }
});
if (!existingThread) {
console.log(`Thread ${threadId} not found, skipping deletion`);
return; // Thread doesn't exist, nothing to delete
}
// Prisma will handle cascade deletion due to the schema
await prisma.thread.delete({
where: { id: threadId }
});
}
catch (error) {
console.error('❌ Failed to delete thread:', error);
// If it's a "record not found" error, don't throw - thread is already gone
if (error.code === 'P2025') {
console.log('Thread was already deleted, continuing...');
return;
}
throw error;
}
}
static async disconnect() {
await prisma.$disconnect();
}
}