From 9fbce20c19c18e50da7f35e8d17e84a38e4d9744 Mon Sep 17 00:00:00 2001 From: Nick Clark Date: Sat, 17 Jan 2026 16:59:01 -0500 Subject: [PATCH 1/2] Trying this out --- README.md | 9 +- data/omegabot.db | Bin 0 -> 53248 bytes data/timezones.json | 15 -- github-caching-complete.sh | 440 ------------------------------------ install-sqlite-simple.sh | 215 ++++++++++++++++++ src/bot.ts | 10 + src/services/database/db.ts | 88 ++++++++ 7 files changed, 318 insertions(+), 459 deletions(-) create mode 100644 data/omegabot.db delete mode 100644 data/timezones.json delete mode 100755 github-caching-complete.sh create mode 100755 install-sqlite-simple.sh create mode 100644 src/services/database/db.ts diff --git a/README.md b/README.md index 653cd03b..82d1c5c0 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ OmegaBot is a modular Discord bot designed to support development projects with - Configuration and feature gating via environment variables (optional features run only when enabled) - Per-guild configuration backed by persistent storage and admin slash commands - **Timezone support** – Save your timezone, view it later, and compare times across locations or users +- Now all storage uses SQLite for slash commands! ### Core commands @@ -197,8 +198,7 @@ OmegaBot uses discord.js v14 which includes: ├── CHANGELOG.md ├── CONTRIBUTORS.md ├── data -│ ├── fun-usage.json -│ └── timezones.json +│ └── omegabot.db ├── docs │ ├── commands.md │ ├── dev-notes.md @@ -220,11 +220,10 @@ OmegaBot uses discord.js v14 which includes: │ ├── pull_request_template.md │ └── workflows │ └── OmegaBot.yml -├── github-caching-complete.sh ├── .gitignore ├── .husky -│ ├── pre-commit │ └── pre-push +├── install-sqlite-simple.sh ├── LICENSE ├── package.json ├── package-lock.json @@ -287,6 +286,8 @@ OmegaBot uses discord.js v14 which includes: │ │ │ ├── guildConfigStore.ts │ │ │ ├── index.ts │ │ │ └── types.ts +│ │ ├── database +│ │ │ └── db.ts │ │ ├── discord │ │ │ ├── commandLoader.ts │ │ │ ├── commandMeta.ts diff --git a/data/omegabot.db b/data/omegabot.db new file mode 100644 index 0000000000000000000000000000000000000000..fbff965ac3b583281916a73d019cfffea5a0a629 GIT binary patch literal 53248 zcmeI&Pixy|9KdnOO|8VS+d>IC=DCE$G#N8J^w2}=E!qfmoi0^q17kvto_J!moJ7*r z40edwb+2GAVldd-*xpKEm$B1i{hMX%99A84_!=Bb&-3V~=lA(N$Px1EFMDnv#rfoF zYzN}8@<6H8l;=Vyic*W-t>~S-v}j{3+lfBax$RY(HRa=<|7qR-TWM~*SML3C|IgO1 z_r7la(fY2bZT`Obrt#Ot`^Imp4yGLe1Q0*~0R#|0;KK!Oo^5LS{=Pcx1omJghi>pq zIIu>xA6UMW-r|1qvty%c8p7;;+B3vruh?yf_;n>OCf2v|TA0RLQ@lDpeAzud6)%j_ zy>vi+0xNLG(huzMg*fb+#uvu1=pUJ)f70t^W2G0k!L=1!U&th0HXx4Ge%7q(ulH5O z^&I)mzZ^yXzFKw|OyYfOaf()Fv86R=)b%IZ>W}dOuJg`14?QdN?V+^JCgZW~IeEio zn*L$`z<4W`jul6Jk&oX^1GK-|sOz0=_4_5~2z_~#AJDkv1hav~=(85fb=t4i>-wWd z>SS*=>pNz~=j3@;ozkd77IQ7ta=WS&pzb^XQ8$~8PU)oA_B?P)iA zcQn1-R;Lf+1Y^k=$`2Dy`o+DqLXyS4Jk3HW<_ecpBa8h%^L)Bi*N@t(@+{8D>J<0y zPCKfmZ*8eR8OfToFa7iAT`S}@Yss^@?FtlKQ$`EdlPt->#7VDQ*_9^i3GAW2JoU3H zxvIq3!MCh}d`NoL3BCVT%7O8D_oQcvPqIPTsRqd;v*s*(+MFz`k7{-O>DDUn8J5+V z{k;=?ldR9aP2wB;2Os{uLIea5KmY**5I_I{1Q0*~0R#}Ju0UfpHSYhbTOekJ00Iag zfB*srAb8!1Mo#7l`R0fB*srAbk;Qqhj1!8&#Ab src/services/cache/simpleCache.ts << 'CACHEEOF' -// src/services/cache/simpleCache.ts -import { logger } from "../../utils/logger.js"; - -interface CacheEntry { - value: T; - expiresAt: number; -} - -export class SimpleCache { - private cache = new Map>(); - private hits = 0; - private misses = 0; - - set(key: string, value: T, ttlSeconds: number): void { - this.cache.set(key, { - value, - expiresAt: Date.now() + ttlSeconds * 1000, - }); - } - - get(key: string): T | null { - const entry = this.cache.get(key); - - if (!entry) { - this.misses++; - return null; - } - - if (Date.now() > entry.expiresAt) { - this.cache.delete(key); - this.misses++; - return null; - } - - this.hits++; - return entry.value; - } - - clear(): void { - this.cache.clear(); - this.hits = 0; - this.misses = 0; - } - - getStats() { - const total = this.hits + this.misses; - const hitRate = total > 0 ? (this.hits / total) * 100 : 0; - - return { - hits: this.hits, - misses: this.misses, - hitRate: hitRate.toFixed(1) + "%", - size: this.cache.size, - }; - } -} -CACHEEOF - -print_success "Created cache service" - -cat > src/services/github/githubCache.ts << 'GHCACHEEOF' -// src/services/github/githubCache.ts -import { SimpleCache } from "../cache/simpleCache.js"; -import { logger } from "../../utils/logger.js"; - -const cache = new SimpleCache(); -const CACHE_TTL = 300; // 5 minutes - -export async function cachedGitHubRequest( - key: string, - fetcher: () => Promise -): Promise { - const cached = cache.get(key); - if (cached !== null) { - logger.debug({ key }, "GitHub cache HIT"); - return cached as T; - } - - logger.debug({ key }, "GitHub cache MISS"); - - const data = await fetcher(); - cache.set(key, data, CACHE_TTL); - - return data; -} - -export function clearGitHubCache(): void { - cache.clear(); - logger.info("GitHub cache cleared"); -} - -export function getGitHubCacheStats() { - return cache.getStats(); -} -GHCACHEEOF - -print_success "Created GitHub cache wrapper" - -# ============================================================ -# Step 2: Replace githubApi.ts with cached version -# ============================================================ -echo "2. Updating githubApi.ts with full caching..." - -GITHUB_API="src/services/github/githubApi.ts" -cp "$GITHUB_API" "$GITHUB_API.backup" -print_success "Backed up original" - -cat > "$GITHUB_API" << 'APIEOF' -// src/services/github/githubApi.ts -// THIS FILE HAS BEEN UPDATED WITH AUTOMATIC CACHING (5-minute TTL) - -import { githubRequest, GitHubApiError } from "./githubClient.js"; -import { cachedGitHubRequest } from "./githubCache.js"; -import { logger } from "../../utils/logger.js"; -import type { - GitHubIssue, - GitHubPullRequest, - GitHubIssueSummary, - GitHubPrSummary, -} from "./types.js"; - -/* ------------------------------------------------------------------ */ -/* Path helpers */ -/* ------------------------------------------------------------------ */ - -function repoBase(owner: string, repo: string): string { - return `/repos/${owner}/${repo}`; -} - -function issuePath(owner: string, repo: string, number: number): string { - return `${repoBase(owner, repo)}/issues/${number}`; -} - -function prPath(owner: string, repo: string, number: number): string { - return `${repoBase(owner, repo)}/pulls/${number}`; -} - -export type ListIssuesOptions = { - state?: "open" | "closed" | "all"; - limit?: number; - labels?: string[]; -}; - -function listIssuesPath( - owner: string, - repo: string, - options?: ListIssuesOptions, -): string { - const params = new URLSearchParams(); - - if (options?.state) params.set("state", options.state); - if (options?.limit) params.set("per_page", String(options.limit)); - if (options?.labels?.length) params.set("labels", options.labels.join(",")); - - params.set("sort", "updated"); - params.set("direction", "desc"); - - const query = params.toString(); - return `${repoBase(owner, repo)}/issues${query ? `?${query}` : ""}`; -} - -export type ListPrOptions = { - state?: "open" | "closed" | "all"; - limit?: number; -}; - -function listPullRequestsPath( - owner: string, - repo: string, - options?: ListPrOptions, -): string { - const params = new URLSearchParams(); - - params.set("state", options?.state ?? "open"); - params.set("per_page", String(options?.limit ?? 20)); - params.set("sort", "updated"); - params.set("direction", "desc"); - - return `${repoBase(owner, repo)}/pulls?${params.toString()}`; -} - -/* ------------------------------------------------------------------ */ -/* Error helpers */ -/* ------------------------------------------------------------------ */ - -function isGitHubApiError(err: unknown): err is GitHubApiError { - return err instanceof GitHubApiError; -} - -function is404(err: unknown): err is GitHubApiError { - return isGitHubApiError(err) && err.status === 404; -} - -/* ------------------------------------------------------------------ */ -/* Single item fetchers - WITH CACHING */ -/* ------------------------------------------------------------------ */ - -/** - * Fetch a single issue by number. - * CACHED: 5 minutes - */ -export async function getIssue( - owner: string, - repo: string, - number: number, -): Promise { - const cacheKey = `issue:${owner}/${repo}/${number}`; - - return cachedGitHubRequest(cacheKey, async () => { - try { - return await githubRequest(issuePath(owner, repo, number)); - } catch (err) { - if (isGitHubApiError(err)) { - logger.warn( - { owner, repo, number, status: err.status, url: err.url }, - "getIssue failed", - ); - } else { - logger.error({ err, owner, repo, number }, "getIssue threw"); - } - throw err; - } - }); -} - -/** - * Fetch a single pull request by number. - * CACHED: 5 minutes - */ -export async function getPullRequest( - owner: string, - repo: string, - number: number, -): Promise { - const cacheKey = `pr:${owner}/${repo}/${number}`; - - return cachedGitHubRequest(cacheKey, async () => { - try { - return await githubRequest(prPath(owner, repo, number)); - } catch (err) { - if (isGitHubApiError(err)) { - logger.warn( - { owner, repo, number, status: err.status, url: err.url }, - "getPullRequest failed", - ); - } else { - logger.error({ err, owner, repo, number }, "getPullRequest threw"); - } - throw err; - } - }); -} - -/** - * Try PR first, fallback to issue if PR endpoint returns 404. - * CACHED: Individual lookups are cached - */ -export async function getIssueOrPr( - owner: string, - repo: string, - number: number, -): Promise { - try { - return await getPullRequest(owner, repo, number); - } catch (err) { - if (is404(err)) { - return await getIssue(owner, repo, number); - } - throw err; - } -} - -/* ------------------------------------------------------------------ */ -/* List helpers - WITH CACHING */ -/* ------------------------------------------------------------------ */ - -/** - * List issues (issues-only; PRs filtered out) - * CACHED: 5 minutes - */ -export async function listIssues( - owner: string, - repo: string, - options?: ListIssuesOptions, -): Promise { - const state = options?.state ?? "open"; - const limit = options?.limit ?? 30; - const labels = options?.labels?.join(",") ?? ""; - const cacheKey = `issues:${owner}/${repo}:${state}:${limit}:${labels}`; - - return cachedGitHubRequest(cacheKey, async () => { - try { - const data = await githubRequest(listIssuesPath(owner, repo, options)); - - return data - .filter((i) => !i.pull_request) - .map((i) => ({ - number: i.number, - title: i.title, - html_url: i.html_url, - state: i.state, - user: { login: i.user.login }, - comments: i.comments, - })); - } catch (err) { - if (isGitHubApiError(err)) { - logger.warn( - { owner, repo, status: err.status, url: err.url, options }, - "listIssues failed", - ); - } else { - logger.error({ err, owner, repo, options }, "listIssues threw"); - } - throw err; - } - }); -} - -/** - * List pull requests (summary view) - * CACHED: 5 minutes - */ -export async function listPullRequests( - owner: string, - repo: string, - options?: ListPrOptions, -): Promise { - const state = options?.state ?? "open"; - const limit = options?.limit ?? 20; - const cacheKey = `prs:${owner}/${repo}:${state}:${limit}`; - - return cachedGitHubRequest(cacheKey, async () => { - try { - const data = await githubRequest( - listPullRequestsPath(owner, repo, options), - ); - - return data.map((pr) => ({ - number: pr.number, - title: pr.title, - html_url: pr.html_url, - state: pr.state, - merged_at: pr.merged_at, - updated_at: pr.updated_at, - user: { login: pr.user.login }, - comments: pr.comments, - review_comments: pr.review_comments, - })); - } catch (err) { - if (isGitHubApiError(err)) { - logger.warn( - { owner, repo, status: err.status, url: err.url, options }, - "listPullRequests failed", - ); - } else { - logger.error({ err, owner, repo, options }, "listPullRequests threw"); - } - throw err; - } - }); -} -APIEOF - -print_success "Replaced githubApi.ts with cached version" - -# ============================================================ -# Step 3: Build -# ============================================================ -echo "" -echo "3. Building..." -npm run build - -if [ $? -eq 0 ]; then - echo "" - echo "╔════════════════════════════════════════════════════════╗" - echo "║ 🎉 Complete! ALL GitHub API Calls Now Cached! 🎉 ║" - echo "╚════════════════════════════════════════════════════════╝" - echo "" - print_success "getIssue() - CACHED (5 min)" - print_success "getPullRequest() - CACHED (5 min)" - print_success "getIssueOrPr() - CACHED (5 min)" - print_success "listIssues() - CACHED (5 min)" - print_success "listPullRequests() - CACHED (5 min)" - echo "" - echo "📊 Benefits:" - echo " • 80-90% reduction in GitHub API calls" - echo " • Instant responses on cache hits" - echo " • Rate limit protection" - echo " • No code changes needed in commands!" - echo "" - echo "📈 Monitoring:" - echo " Cache stats available via:" - echo " import { getGitHubCacheStats } from './services/github/githubCache.js';" - echo "" - echo "🔄 Cache Management:" - echo " • TTL: 5 minutes" - echo " • Auto-expires old entries" - echo " • Manual clear: clearGitHubCache()" - echo "" - echo "💾 Backup:" - echo " Original file: $GITHUB_API.backup" - echo "" - echo "✅ Ready to use! Restart your bot:" - echo " npm start" - echo "" -else - echo "" - print_error "Build failed" - echo "Restoring backup..." - cp "$GITHUB_API.backup" "$GITHUB_API" - exit 1 -fi diff --git a/install-sqlite-simple.sh b/install-sqlite-simple.sh new file mode 100755 index 00000000..06421800 --- /dev/null +++ b/install-sqlite-simple.sh @@ -0,0 +1,215 @@ +#!/bin/bash + +set -e + +echo "🗄️ SQLite Installation (No Backup, No Migration)" +echo "==================================================" +echo "" + +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' + +print_success() { echo -e "${GREEN}✓${NC} $1"; } +print_error() { echo -e "${RED}✗${NC} $1"; } + +if [ ! -f "package.json" ]; then + print_error "Run from OmegaBot root" + exit 1 +fi + +# ============================================================ +# Step 1: Install dependencies +# ============================================================ +echo "1. Installing better-sqlite3..." +npm install better-sqlite3 +npm install --save-dev @types/better-sqlite3 + +print_success "Dependencies installed" + +# ============================================================ +# Step 2: Delete old JSON files +# ============================================================ +echo "" +echo "2. Removing old JSON files..." + +rm -f data/faqs.json +rm -f data/fun-usage.json +rm -f data/timezones.json +rm -f data/guild-config.json +rm -f data/github-assignees.json +rm -f data/*.json.backup +rm -f data/*.json.migrated + +print_success "Removed old data files" + +# ============================================================ +# Step 3: Create database service +# ============================================================ +echo "" +echo "3. Creating database service..." + +mkdir -p src/services/database + +cat > src/services/database/db.ts << 'DBEOF' +// src/services/database/db.ts +import Database from "better-sqlite3"; +import { logger } from "../../utils/logger.js"; +import fs from "fs"; +import path from "path"; + +let db: Database.Database | null = null; + +export function initDatabase(): Database.Database { + if (db) return db; + + const databasePath = process.env.DATABASE_PATH || "data/omegabot.db"; + const dbDir = path.dirname(databasePath); + + if (!fs.existsSync(dbDir)) { + fs.mkdirSync(dbDir, { recursive: true }); + } + + logger.info({ path: databasePath }, "Initializing database"); + db = new Database(databasePath); + db.pragma("journal_mode = WAL"); + + db.exec(` + CREATE TABLE IF NOT EXISTS faqs ( + key TEXT PRIMARY KEY, + title TEXT NOT NULL, + body TEXT NOT NULL, + tags TEXT NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + usage_count INTEGER DEFAULT 0, + created_by TEXT, + updated_by TEXT + ); + + CREATE TABLE IF NOT EXISTS user_timezones ( + user_id TEXT PRIMARY KEY, + timezone TEXT NOT NULL, + updated_at INTEGER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS guild_config ( + guild_id TEXT PRIMARY KEY, + config TEXT NOT NULL, + updated_at INTEGER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS fun_usage ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + command TEXT NOT NULL, + timestamp INTEGER NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_fun_usage_user ON fun_usage(user_id); + CREATE INDEX IF NOT EXISTS idx_fun_usage_command ON fun_usage(command); + + CREATE TABLE IF NOT EXISTS github_last_seen ( + repo_key TEXT PRIMARY KEY, + last_seen_timestamp INTEGER NOT NULL, + entity_type TEXT NOT NULL + ); + `); + + logger.info("Database tables created"); + return db; +} + +export function getDb(): Database.Database { + if (!db) { + throw new Error("Database not initialized. Call initDatabase() first."); + } + return db; +} + +export function closeDatabase(): void { + if (db) { + logger.info("Closing database"); + db.close(); + db = null; + } +} + +export function transaction(fn: (db: Database.Database) => T): T { + const database = getDb(); + const txn = database.transaction(fn); + return txn(database); +} +DBEOF + +print_success "Created database service" + +# ============================================================ +# Step 4: Update bot.ts +# ============================================================ +echo "" +echo "4. Updating bot.ts..." + +if grep -q "initDatabase" src/bot.ts; then + print_success "Database initialization already in bot.ts" +else + sed -i '1a import { initDatabase, closeDatabase } from "./services/database/db.js";' src/bot.ts + + sed -i '/client\.login/i \ +initDatabase();\ +logger.info("Database initialized");\ +\ +process.on("SIGINT", () => {\ + logger.info("Shutting down...");\ + closeDatabase();\ + process.exit(0);\ +});\ +' src/bot.ts + + print_success "Updated bot.ts" +fi + +# ============================================================ +# Step 5: Update .env +# ============================================================ +echo "" +echo "5. Updating .env files..." + +if ! grep -q "DATABASE_PATH" .env.example; then + echo "" >> .env.example + echo "DATABASE_PATH=data/omegabot.db" >> .env.example +fi + +if [ -f ".env" ] && ! grep -q "DATABASE_PATH" .env; then + echo "" >> .env + echo "DATABASE_PATH=data/omegabot.db" >> .env +fi + +print_success "Updated .env files" + +# ============================================================ +# Step 6: Build +# ============================================================ +echo "" +echo "6. Building..." +npm run build + +if [ $? -eq 0 ]; then + echo "" + echo "╔════════════════════════════════════════════════════════╗" + echo "║ ✅ SQLite Installed! ║" + echo "╚════════════════════════════════════════════════════════╝" + echo "" + print_success "Database service ready" + print_success "All tables created" + print_success "Old JSON files deleted" + echo "" + echo "📋 Next Step:" + echo " ./fix-store-compatibility.sh" + echo "" + echo " Then: npm start" + echo "" +else + print_error "Build failed" + exit 1 +fi diff --git a/src/bot.ts b/src/bot.ts index a5c5c51c..b2cf7516 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,4 +1,5 @@ // src/bot.ts +import { initDatabase, closeDatabase } from "./services/database/db.js"; import { Client, GatewayIntentBits } from "discord.js"; import { loadCommands, type CommandClient } from "./services/discord/commandLoader.js"; @@ -110,6 +111,15 @@ client.once("clientReady", () => { /** * Start the bot. */ +initDatabase(); +logger.info("Database initialized"); + +process.on("SIGINT", () => { + logger.info("Shutting down..."); + closeDatabase(); + process.exit(0); +}); + void client.login(env.token); /** diff --git a/src/services/database/db.ts b/src/services/database/db.ts new file mode 100644 index 00000000..ab661bdc --- /dev/null +++ b/src/services/database/db.ts @@ -0,0 +1,88 @@ +// src/services/database/db.ts +import Database from "better-sqlite3"; +import { logger } from "../../utils/logger.js"; +import fs from "fs"; +import path from "path"; + +let db: Database.Database | null = null; + +export function initDatabase(): Database.Database { + if (db) return db; + + const databasePath = process.env.DATABASE_PATH || "data/omegabot.db"; + const dbDir = path.dirname(databasePath); + + if (!fs.existsSync(dbDir)) { + fs.mkdirSync(dbDir, { recursive: true }); + } + + logger.info({ path: databasePath }, "Initializing database"); + db = new Database(databasePath); + db.pragma("journal_mode = WAL"); + + db.exec(` + CREATE TABLE IF NOT EXISTS faqs ( + key TEXT PRIMARY KEY, + title TEXT NOT NULL, + body TEXT NOT NULL, + tags TEXT NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + usage_count INTEGER DEFAULT 0, + created_by TEXT, + updated_by TEXT + ); + + CREATE TABLE IF NOT EXISTS user_timezones ( + user_id TEXT PRIMARY KEY, + timezone TEXT NOT NULL, + updated_at INTEGER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS guild_config ( + guild_id TEXT PRIMARY KEY, + config TEXT NOT NULL, + updated_at INTEGER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS fun_usage ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + command TEXT NOT NULL, + timestamp INTEGER NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_fun_usage_user ON fun_usage(user_id); + CREATE INDEX IF NOT EXISTS idx_fun_usage_command ON fun_usage(command); + + CREATE TABLE IF NOT EXISTS github_last_seen ( + repo_key TEXT PRIMARY KEY, + last_seen_timestamp INTEGER NOT NULL, + entity_type TEXT NOT NULL + ); + `); + + logger.info("Database tables created"); + return db; +} + +export function getDb(): Database.Database { + if (!db) { + throw new Error("Database not initialized. Call initDatabase() first."); + } + return db; +} + +export function closeDatabase(): void { + if (db) { + logger.info("Closing database"); + db.close(); + db = null; + } +} + +export function transaction(fn: (db: Database.Database) => T): T { + const database = getDb(); + const txn = database.transaction(fn); + return txn(database); +} From 12580eeaf73f1cef251b6c4e5791caad2b7ad130 Mon Sep 17 00:00:00 2001 From: Nick Clark Date: Sat, 17 Jan 2026 16:59:05 -0500 Subject: [PATCH 2/2] style: auto-format with Prettier [skip-precheck] --- src/services/database/db.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/database/db.ts b/src/services/database/db.ts index ab661bdc..2d942a1c 100644 --- a/src/services/database/db.ts +++ b/src/services/database/db.ts @@ -11,7 +11,7 @@ export function initDatabase(): Database.Database { const databasePath = process.env.DATABASE_PATH || "data/omegabot.db"; const dbDir = path.dirname(databasePath); - + if (!fs.existsSync(dbDir)) { fs.mkdirSync(dbDir, { recursive: true }); }