src / dbManager.ts
import fs from "fs";
import path from "path";
export interface Message {
role: "system" | "user" | "assistant";
content: string;
timestamp: string;
}
export interface ImageRecord {
id: number;
conversationId: number;
filename: string;
description: string;
base64Data?: string;
filePath?: string;
createdAt: string;
}
export interface ConversationRecord {
id: number;
title: string;
tags: string[];
summary: string;
messages: Message[];
imageIds: number[];
createdAt: string;
updatedAt: string;
}
export interface Database {
conversations: ConversationRecord[];
images: ImageRecord[];
nextConversationId: number;
nextImageId: number;
}
export class ConversationDB {
private db: Database;
private dbPath: string;
private imagesDir: string;
constructor() {
this.dbPath = path.join(__dirname, "../conversation_db.json");
this.imagesDir = path.join(__dirname, "../images");
this.ensureDirs();
this.db = this.load();
}
private ensureDirs() {
if (!fs.existsSync(this.imagesDir)) {
fs.mkdirSync(this.imagesDir, { recursive: true });
}
}
private load(): Database {
if (fs.existsSync(this.dbPath)) {
try {
const raw = fs.readFileSync(this.dbPath, "utf-8");
const db = JSON.parse(raw) as Database;
db.conversations = db.conversations || [];
db.images = db.images || [];
db.nextConversationId = db.nextConversationId || 1;
db.nextImageId = db.nextImageId || 1;
return db;
} catch {
return this.emptyDb();
}
}
return this.emptyDb();
}
private emptyDb(): Database {
return { conversations: [], images: [], nextConversationId: 1, nextImageId: 1 };
}
private save() {
try {
fs.writeFileSync(this.dbPath, JSON.stringify(this.db, null, 2));
} catch (e) {
console.error("Failed to save conversation_db.json:", e);
}
}
// --- Conversations ---
addConversation(title: string, messages: Message[], tags: string[] = [], summary: string = ""): string {
const id = this.db.nextConversationId++;
const now = new Date().toISOString();
this.db.conversations.push({
id,
title,
tags,
summary,
messages,
imageIds: [],
createdAt: now,
updatedAt: now,
});
this.save();
return `Saved conversation #${id}: "${title}" with ${messages.length} messages`;
}
getConversation(id: number, includeMessages: boolean = true): string {
const conv = this.db.conversations.find((c) => c.id === id);
if (!conv) return `Error: Conversation #${id} not found.`;
let out = `Conversation #${conv.id} — "${conv.title}"\n`;
out += `Created: ${conv.createdAt}\n`;
out += `Updated: ${conv.updatedAt}\n`;
out += `Tags: ${conv.tags.join(", ") || "(none)"}\n`;
out += `Summary: ${conv.summary || "(none)"}\n`;
out += `Messages: ${conv.messages.length}\n`;
out += `Images: ${conv.imageIds.length}\n`;
if (includeMessages && conv.messages.length > 0) {
out += "\n--- Messages ---\n";
for (const m of conv.messages) {
out += `[${m.role} @ ${m.timestamp}]\n${m.content}\n\n`;
}
}
return out;
}
listConversations(tag?: string, limit: number = 20): string {
let list = this.db.conversations;
if (tag) {
list = list.filter((c) => c.tags.some((t) => t.toLowerCase().includes(tag.toLowerCase())));
}
list = [...list].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
list = list.slice(0, limit);
if (list.length === 0) return "No conversations found.";
let out = `--- CONVERSATIONS (${list.length} shown) ---\n`;
for (const c of list) {
out += `[#${c.id}] "${c.title}" | Tags: ${c.tags.join(", ") || "(none)"} | Updated: ${c.updatedAt}\n`;
}
return out;
}
searchConversations(query: string, limit: number = 10): string {
const q = query.toLowerCase();
const results = this.db.conversations.filter(
(c) =>
c.title.toLowerCase().includes(q) ||
c.summary.toLowerCase().includes(q) ||
c.tags.some((t) => t.toLowerCase().includes(q)) ||
c.messages.some((m) => m.content.toLowerCase().includes(q))
);
const sorted = [...results].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
const limited = sorted.slice(0, limit);
if (limited.length === 0) return `No conversations matching "${query}".`;
let out = `--- SEARCH: "${query}" (${limited.length} results) ---\n`;
for (const c of limited) {
out += `[#${c.id}] "${c.title}" | Tags: ${c.tags.join(", ") || "(none)"}\n`;
if (c.summary) out += ` Summary: ${c.summary}\n`;
}
return out;
}
updateConversation(id: number, updates: { title?: string; tags?: string[]; summary?: string }): string {
const conv = this.db.conversations.find((c) => c.id === id);
if (!conv) return `Error: Conversation #${id} not found.`;
if (updates.title !== undefined) conv.title = updates.title;
if (updates.tags !== undefined) conv.tags = updates.tags;
if (updates.summary !== undefined) conv.summary = updates.summary;
conv.updatedAt = new Date().toISOString();
this.save();
return `Updated conversation #${id}: "${conv.title}"`;
}
deleteConversation(id: number): string {
const before = this.db.conversations.length;
const conv = this.db.conversations.find((c) => c.id === id);
if (!conv) return `Error: Conversation #${id} not found.`;
// Delete associated images
for (const imgId of conv.imageIds) {
this.deleteImageFile(imgId);
}
this.db.images = this.db.images.filter((i) => !conv.imageIds.includes(i.id));
this.db.conversations = this.db.conversations.filter((c) => c.id !== id);
this.save();
return `Deleted conversation #${id} and ${conv.imageIds.length} associated images.`;
}
// --- Images ---
addImage(conversationId: number, filename: string, description: string, base64Data?: string): string {
const conv = this.db.conversations.find((c) => c.id === conversationId);
if (!conv) return `Error: Conversation #${conversationId} not found. Create it first.`;
const id = this.db.nextImageId++;
const now = new Date().toISOString();
let filePath: string | undefined;
if (base64Data) {
filePath = path.join(this.imagesDir, `${id}_${filename}`);
try {
const base64 = base64Data.startsWith("data:") ? base64Data.split(",")[1] : base64Data;
fs.writeFileSync(filePath, Buffer.from(base64, "base64"));
} catch (e) {
return `Error: Failed to save image file: ${e}`;
}
}
const record: ImageRecord = { id, conversationId, filename, description, base64Data, filePath, createdAt: now };
this.db.images.push(record);
conv.imageIds.push(id);
conv.updatedAt = now;
this.save();
return `Saved image #${id} "${filename}" to conversation #${conversationId}`;
}
getImage(id: number, includeData: boolean = false): string {
const img = this.db.images.find((i) => i.id === id);
if (!img) return `Error: Image #${id} not found.`;
let out = `Image #${img.id}\n`;
out += `Conversation: #${img.conversationId}\n`;
out += `Filename: ${img.filename}\n`;
out += `Description: ${img.description}\n`;
out += `File path: ${img.filePath || "(not saved to disk)"}\n`;
out += `Created: ${img.createdAt}\n`;
if (includeData && img.base64Data) {
out += `Base64 length: ${img.base64Data.length} chars\n`;
}
return out;
}
listImages(conversationId?: number): string {
let imgs = this.db.images;
if (conversationId !== undefined) {
imgs = imgs.filter((i) => i.conversationId === conversationId);
}
if (imgs.length === 0) return "No images found.";
let out = `--- IMAGES (${imgs.length} total) ---\n`;
for (const i of imgs) {
out += `[#${i.id}] "${i.filename}" | Conv: #${i.conversationId} | ${i.description}\n`;
}
return out;
}
private deleteImageFile(id: number) {
const img = this.db.images.find((i) => i.id === id);
if (img?.filePath && fs.existsSync(img.filePath)) {
try {
fs.unlinkSync(img.filePath);
} catch {
// ignore
}
}
}
deleteImage(id: number): string {
const img = this.db.images.find((i) => i.id === id);
if (!img) return `Error: Image #${id} not found.`;
this.deleteImageFile(id);
// Remove from conversation
const conv = this.db.conversations.find((c) => c.id === img.conversationId);
if (conv) {
conv.imageIds = conv.imageIds.filter((iid) => iid !== id);
conv.updatedAt = new Date().toISOString();
}
this.db.images = this.db.images.filter((i) => i.id !== id);
this.save();
return `Deleted image #${id}.`;
}
// --- Stats ---
stats(): string {
const totalMsgs = this.db.conversations.reduce((sum, c) => sum + c.messages.length, 0);
const totalImgs = this.db.images.length;
let out = `--- DATABASE STATS ---\n`;
out += `Conversations: ${this.db.conversations.length}\n`;
out += `Total messages: ${totalMsgs}\n`;
out += `Images: ${totalImgs}\n`;
if (this.db.conversations.length > 0) {
const tags = new Map<string, number>();
for (const c of this.db.conversations) {
for (const t of c.tags) {
tags.set(t, (tags.get(t) || 0) + 1);
}
}
const topTags = [...tags.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5);
if (topTags.length > 0) {
out += "\nTop tags:\n";
for (const [tag, count] of topTags) {
out += ` ${tag}: ${count}\n`;
}
}
}
return out;
}
clearAll(): string {
const convCount = this.db.conversations.length;
const imgCount = this.db.images.length;
for (const img of this.db.images) {
this.deleteImageFile(img.id);
}
this.db = this.emptyDb();
this.save();
return `Cleared ${convCount} conversations and ${imgCount} images.`;
}
}