summaryrefslogtreecommitdiffhomepage
path: root/packages/core/src/db
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-21 17:30:08 +0900
committerAdam Malczewski <[email protected]>2026-05-21 17:30:08 +0900
commitd6b208342edf97bafa5b1dcc986b782f9879d141 (patch)
treec6d8f9bffa86f78e3b1369b811bef9642df788d0 /packages/core/src/db
parent1f309ccca20aabbd0ee3fb8fbb3c8192124edd95 (diff)
downloaddispatch-d6b208342edf97bafa5b1dcc986b782f9879d141.tar.gz
dispatch-d6b208342edf97bafa5b1dcc986b782f9879d141.zip
feat: SQLite database for all credentials, keys, wake schedule, and usage cache
- Add SQLite database at ~/.local/share/dispatch/dispatch.db with tables: credentials, api_keys, wake_schedule, usage_cache - Store Claude OAuth credentials in DB with import button in Model Status UI - Store OpenCode/Copilot API keys in DB with paste-to-import modal - Store OpenCode cookie and workspace IDs in DB - Migrate wake schedule from .wake-schedule.json to DB - Migrate usage cache from in-memory Map + localStorage to DB - Remove all env var and file fallbacks — DB is the single source of truth - Add seed scripts: bin/import-credentials.ts, bin/seed-opencode-keys.ts - Docker: container runs as host UID/GID with matching home directory - Clean up dispatch.toml: remove env fields, update comments - Progress bar time markers for usage cycle tracking
Diffstat (limited to 'packages/core/src/db')
-rw-r--r--packages/core/src/db/index.ts93
1 files changed, 93 insertions, 0 deletions
diff --git a/packages/core/src/db/index.ts b/packages/core/src/db/index.ts
new file mode 100644
index 0000000..818aff0
--- /dev/null
+++ b/packages/core/src/db/index.ts
@@ -0,0 +1,93 @@
+import { Database } from "bun:sqlite";
+import { existsSync, mkdirSync } from "node:fs";
+import { isAbsolute, join } from "node:path";
+import { homedir } from "node:os";
+
+/**
+ * Returns the directory for persistent Dispatch data, following XDG Base
+ * Directory spec on Linux: `$XDG_DATA_HOME/dispatch` (defaults to
+ * `~/.local/share/dispatch`).
+ */
+function getDataDir(): string {
+ const xdg = process.env.XDG_DATA_HOME;
+ const base = xdg && isAbsolute(xdg) ? xdg : join(homedir(), ".local", "share");
+ return join(base, "dispatch");
+}
+
+let _db: Database | null = null;
+
+/**
+ * Get (or create) the singleton SQLite database.
+ *
+ * - Creates the data directory if it doesn't exist.
+ * - Creates `dispatch.db` if it doesn't exist.
+ * - Enables WAL journal mode for concurrent read performance.
+ */
+export function getDatabase(): Database {
+ if (_db) return _db;
+
+ const dir = getDataDir();
+ if (!existsSync(dir)) {
+ mkdirSync(dir, { recursive: true });
+ }
+
+ const dbPath = join(dir, "dispatch.db");
+ _db = new Database(dbPath, { create: true });
+
+ // WAL mode: better concurrent read performance, safe for single-writer
+ _db.run("PRAGMA journal_mode = WAL;");
+ // Recommended for WAL: normal synchronous is safe and faster
+ _db.run("PRAGMA synchronous = NORMAL;");
+ // Enable foreign keys
+ _db.run("PRAGMA foreign_keys = ON;");
+
+ // Create tables
+ _db.run(`CREATE TABLE IF NOT EXISTS credentials (
+ key_id TEXT PRIMARY KEY,
+ provider TEXT NOT NULL,
+ access_token TEXT NOT NULL,
+ refresh_token TEXT NOT NULL,
+ expires_at INTEGER NOT NULL,
+ subscription_type TEXT,
+ source_file TEXT,
+ imported_at INTEGER NOT NULL,
+ updated_at INTEGER NOT NULL
+ )`);
+
+ _db.run(`CREATE TABLE IF NOT EXISTS wake_schedule (
+ hour INTEGER PRIMARY KEY CHECK (hour BETWEEN 0 AND 23),
+ next_wake_at INTEGER NOT NULL
+ )`);
+
+ _db.run(`CREATE TABLE IF NOT EXISTS usage_cache (
+ key_id TEXT PRIMARY KEY,
+ provider TEXT NOT NULL,
+ cached_at INTEGER NOT NULL,
+ report_json TEXT NOT NULL
+ )`);
+
+ _db.run(`CREATE TABLE IF NOT EXISTS api_keys (
+ key_id TEXT PRIMARY KEY,
+ provider TEXT NOT NULL,
+ api_key TEXT NOT NULL,
+ imported_at INTEGER NOT NULL,
+ updated_at INTEGER NOT NULL
+ )`);
+
+ return _db;
+}
+
+/** Close the database connection (e.g. on shutdown). */
+export function closeDatabase(): void {
+ if (_db) {
+ _db.close();
+ _db = null;
+ }
+}
+
+/** Returns the path where the database file lives (or will live). */
+export function getDatabasePath(): string {
+ if (_db) return _db.filename;
+ const dir = getDataDir();
+ return join(dir, "dispatch.db");
+}